diff --git a/Assets/000_assets/material/M_SquareFrame 1.mat b/Assets/000_assets/material/M_SquareFrame 1.mat index 9e4741a1..b2deb1a1 100644 --- a/Assets/000_assets/material/M_SquareFrame 1.mat +++ b/Assets/000_assets/material/M_SquareFrame 1.mat @@ -213,7 +213,7 @@ Material: - _Dst: 10 - _DstBlend: 0 - _DstBlendAlpha: 0 - - _EdgeValue: 0.6926449 + - _EdgeValue: 0.91334355 - _EnvironmentReflections: 1 - _FNLfanxiangkaiguan: 0 - _Face: 1 @@ -258,7 +258,7 @@ Material: - _Mask_scale: 1 - _Metallic: 0 - _OcclusionStrength: 1 - - _Opacity: 0.3073551 + - _Opacity: 0.08665645 - _Parallax: 0.005 - _Pass: 0 - _QueueOffset: 0 diff --git a/Assets/FR2_Cache.asset b/Assets/FR2_Cache.asset index e486af1d..88fe0b9e 100644 --- a/Assets/FR2_Cache.asset +++ b/Assets/FR2_Cache.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:633727a3dc99f3fce21a3678c87748ccfd35662d9ff3fc9e745fb6025f286408 -size 7049278 +oid sha256:96247433ef4d17603821fda6b104328124ab0d4c7b47813553c6bd4a05d76fac +size 7130126 diff --git a/Assets/Plugins/Easy Save 3/EasySave3.asmdef b/Assets/Plugins/Easy Save 3/EasySave3.asmdef new file mode 100644 index 00000000..6c5b378e --- /dev/null +++ b/Assets/Plugins/Easy Save 3/EasySave3.asmdef @@ -0,0 +1,17 @@ +{ + "name": "EasySave3", + "rootNamespace": "", + "references": [ + "GUID:6055be8ebefd69e48b49212b09b47b2f", + "GUID:21b0c8d1703a94250bfac916590cea4f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Plugins/Easy Save 3/EasySave3.asmdef.meta b/Assets/Plugins/Easy Save 3/EasySave3.asmdef.meta new file mode 100644 index 00000000..afeac121 --- /dev/null +++ b/Assets/Plugins/Easy Save 3/EasySave3.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: cfcd2ce455f8d1944942cdd919ecaa60 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Easy Save 3/Editor.meta b/Assets/Plugins/Easy Save 3/Editor.meta index 4ee985ca..7f7a7e17 100644 --- a/Assets/Plugins/Easy Save 3/Editor.meta +++ b/Assets/Plugins/Easy Save 3/Editor.meta @@ -2,7 +2,7 @@ fileFormatVersion: 2 guid: 600dbc665993148f7b59ae7356fe862e folderAsset: yes timeCreated: 1474041532 -licenseType: Store +licenseType: Free DefaultImporter: userData: assetBundleName: diff --git a/Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef b/Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef new file mode 100644 index 00000000..40298f5d --- /dev/null +++ b/Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "EasySave3Editor", + "rootNamespace": "", + "references": [ + "GUID:cfcd2ce455f8d1944942cdd919ecaa60" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef.meta b/Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef.meta new file mode 100644 index 00000000..59b36c57 --- /dev/null +++ b/Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: eb59b970a7d779141b50532b8cafd00f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Plugins/Easy Save 3/Resources/ES3/ES3Defaults.asset b/Assets/Plugins/Easy Save 3/Resources/ES3/ES3Defaults.asset index 8d6bf384..a0bbd10b 100644 --- a/Assets/Plugins/Easy Save 3/Resources/ES3/ES3Defaults.asset +++ b/Assets/Plugins/Easy Save 3/Resources/ES3/ES3Defaults.asset @@ -1,71 +1,3 @@ -%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: 7b340139c9e4d054f904d8b452798652, type: 3} - m_Name: ES3Defaults - m_EditorClassIdentifier: - settings: - _location: 0 - path: SaveFile.es3 - encryptionType: 0 - compressionType: 0 - encryptionPassword: password - directory: 0 - format: 0 - prettyPrint: 1 - bufferSize: 2048 - saveChildren: 1 - postprocessRawCachedData: 0 - storeCacheAtEndOfEveryFrame: 1 - storeCacheOnApplicationQuit: 1 - storeCacheOnApplicationPause: 1 - autoCacheDefaultFile: 1 - autoCacheFileOnLoad: 1 - typeChecking: 1 - safeReflection: 1 - memberReferenceMode: 0 - referenceMode: 2 - serializationDepthLimit: 64 - assemblyNames: - - Ak.Wwise.Api.WAAPI - - AK.Wwise.Unity.API - - AK.Wwise.Unity.API.WwiseTypes - - AK.Wwise.Unity.MonoBehaviour - - AK.Wwise.Unity.Timeline - - AK.Wwise.Unity.Utilities - - Assembly-CSharp - - Assembly-CSharp-firstpass - - Boxophobic.AtmosphericHeightFog.Runtime - - Boxophobic.Utils.Scripts - - CW.Common - - IngameDebugConsole.Runtime - - LeanCommon - - LeanPool - - LeanTouch - - LeTai.TranslucentImage - - LeTai.TranslucentImage.Demo - - UniRx - - UniRx.Examples - - UnityUIExtensions - - Wingman - showAdvancedSettings: 0 - addMgrToSceneAutomatically: 0 - autoUpdateReferences: 1 - addAllPrefabsToManager: 1 - collectDependenciesDepth: 4 - collectDependenciesTimeout: 10 - updateReferencesWhenSceneChanges: 1 - updateReferencesWhenSceneIsSaved: 1 - updateReferencesWhenSceneIsOpened: 1 - referenceFolders: [] - logDebugInfo: 0 - logWarnings: 1 - logErrors: 1 +version https://git-lfs.github.com/spec/v1 +oid sha256:f57135f469a2ccfd5819741b58a929afac2bb211b3325a0b2f3fa4c0d999cede +size 1952 diff --git a/Assets/Scenes/GameScene.unity b/Assets/Scenes/GameScene.unity index 52a6392d..e9eb3100 100644 --- a/Assets/Scenes/GameScene.unity +++ b/Assets/Scenes/GameScene.unity @@ -10024,6 +10024,9 @@ MonoBehaviour: - Name: Entry: 8 Data: + - Name: updateScheduler + Entry: 6 + Data: songPlayer: {fileID: 1150174748} cameraManager: {fileID: 1150174742} noteJudgeManager: {fileID: 1332204027} diff --git a/Assets/Scenes/MenuScene.unity b/Assets/Scenes/MenuScene.unity index 66471f19..00d362fd 100644 --- a/Assets/Scenes/MenuScene.unity +++ b/Assets/Scenes/MenuScene.unity @@ -440,18 +440,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 28457517} m_CullTransparentMesh: 1 ---- !u!114 &28725820 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6504848274583113892, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 2142257916} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &30533137 GameObject: m_ObjectHideFlags: 0 @@ -787,18 +775,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 39339085} m_CullTransparentMesh: 1 ---- !u!114 &41005621 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1890038081481639440, guid: 02b942a4ac1a0e047bcc7cf6fc2664e0, - type: 3} - m_PrefabInstance: {fileID: 688756793} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &48744551 GameObject: m_ObjectHideFlags: 0 @@ -2271,18 +2247,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 179146692} m_CullTransparentMesh: 1 ---- !u!114 &188698197 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 2474312952025664795, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 805060254} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &190695849 GameObject: m_ObjectHideFlags: 0 @@ -2486,18 +2450,6 @@ RectTransform: m_AnchoredPosition: {x: 400, y: -227.5} m_SizeDelta: {x: 768, y: 97} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &209046783 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1722593142608310948, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 1609000271} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &213267826 GameObject: m_ObjectHideFlags: 0 @@ -2612,18 +2564,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: -210.00002} m_SizeDelta: {x: 600, y: 100} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &233358388 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6504848274583113892, guid: cc732506675db1f4b9bf9bf0c997748c, - type: 3} - m_PrefabInstance: {fileID: 1951870066} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &242387626 GameObject: m_ObjectHideFlags: 0 @@ -3283,23 +3223,6 @@ MonoBehaviour: mGUI_ShowCallback: 0 mLocalizeTarget: {fileID: 1117633449} mLocalizeTargetName: I2.Loc.LocalizeTarget_TextMeshPro_UGUI ---- !u!114 &298346379 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1781454272} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &314360844 GameObject: m_ObjectHideFlags: 0 @@ -3388,23 +3311,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: cd3a2e144d4d21649b01161673fbdb68, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!114 &320049225 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1258571401} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &325881261 GameObject: m_ObjectHideFlags: 0 @@ -3446,9 +3352,9 @@ MonoBehaviour: m_GameObject: {fileID: 325881261} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 86e86add9ce9c434e8022154ae0c00e7, type: 3} + m_Script: {fileID: 11500000, guid: 0aca77fb4d2655046a9ed2ccd2c14321, type: 3} m_Name: - m_EditorClassIdentifier: Assembly-CSharp::SLSUtilities.WwiseAssistance.AudioManager + m_EditorClassIdentifier: Assembly-CSharp::Ichni.AudioManager serializationData: SerializedFormat: 2 SerializedBytes: @@ -3460,11 +3366,7 @@ MonoBehaviour: SerializationNodes: [] audioPoint: {fileID: 308406596911943559, guid: e3708c94d5457194f93da5c077888870, type: 3} - soundBanks: - - idInternal: 0 - valueGuidInternal: - WwiseObjectReference: {fileID: 11400000, guid: 78145e6a26f414b4d982b957c5d0b363, - type: 2} + soundBanks: [] backgroundMusicManager: {fileID: 0} --- !u!114 &330818074 MonoBehaviour: @@ -4431,18 +4333,6 @@ MonoBehaviour: mAlignment_LTR: 513 mAlignmentWasRTL: 0 mInitializeAlignment: 1 ---- !u!114 &438752297 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1890038081481639440, guid: 02b942a4ac1a0e047bcc7cf6fc2664e0, - type: 3} - m_PrefabInstance: {fileID: 1440750072} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!114 &443068616 MonoBehaviour: m_ObjectHideFlags: 0 @@ -4917,23 +4807,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: f7db12c4a04bb4249a4916541b6d0e5d, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!114 &479886000 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1817954625} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1001 &480190292 PrefabInstance: m_ObjectHideFlags: 0 @@ -5465,23 +5338,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 497105752} m_CullTransparentMesh: 1 ---- !u!114 &497892344 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 209046783} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &506172704 GameObject: m_ObjectHideFlags: 0 @@ -5642,18 +5498,6 @@ MonoBehaviour: settingsButton: {fileID: 1937063705} chapterSelectionUIPrefab: {fileID: 7049295459616353709, guid: f536078f3fe8e764aba92be12b81c2c4, type: 3} ---- !u!114 &514082062 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 4944485236584090101, guid: 8a274db7aa7755b4e8787654931287d1, - type: 3} - m_PrefabInstance: {fileID: 1127235732} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &520601618 GameObject: m_ObjectHideFlags: 0 @@ -5729,23 +5573,6 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 1 ---- !u!114 &524033114 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1665326683} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1001 &532718509 PrefabInstance: m_ObjectHideFlags: 0 @@ -6278,35 +6105,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 582350574} m_CullTransparentMesh: 1 ---- !u!114 &583357335 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 2474312952025664795, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 532718509} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &587811479 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1719155780} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &600935696 GameObject: m_ObjectHideFlags: 0 @@ -6518,18 +6316,6 @@ MonoBehaviour: m_ColorSpace: 0 m_NumColorKeys: 2 m_NumAlphaKeys: 3 ---- !u!114 &618614392 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1722593142608310948, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 480190292} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &619204596 GameObject: m_ObjectHideFlags: 0 @@ -6811,30 +6597,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 621188556} m_CullTransparentMesh: 1 ---- !u!114 &621253541 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 4944485236584090101, guid: 8a274db7aa7755b4e8787654931287d1, - type: 3} - m_PrefabInstance: {fileID: 2004387430697357062} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &622510712 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 4944485236584090101, guid: 8a274db7aa7755b4e8787654931287d1, - type: 3} - m_PrefabInstance: {fileID: 924659615} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &631888791 GameObject: m_ObjectHideFlags: 0 @@ -8549,18 +8311,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 33444f8904d91374fb31960969da96ed, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!114 &720380234 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1722593142608310948, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 805060254} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &732129118 GameObject: m_ObjectHideFlags: 0 @@ -8651,23 +8401,6 @@ MonoBehaviour: musicVolumeModifier: {fileID: 924659617} sfxVolumeModifier: {fileID: 275260321} uiVolumeModifier: {fileID: 471119989} ---- !u!114 &736530267 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1803845447} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &736709066 GameObject: m_ObjectHideFlags: 0 @@ -8743,40 +8476,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 736709066} m_CullTransparentMesh: 1 ---- !u!114 &740707172 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 583357335} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &749248327 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1177858397} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &751614406 GameObject: m_ObjectHideFlags: 0 @@ -9194,18 +8893,6 @@ MonoBehaviour: mAlignment_LTR: 516 mAlignmentWasRTL: 0 mInitializeAlignment: 0 ---- !u!114 &781326709 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6504848274583113892, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 1609000271} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1001 &805060254 PrefabInstance: m_ObjectHideFlags: 0 @@ -10613,23 +10300,6 @@ MonoBehaviour: mAlignment_LTR: 513 mAlignmentWasRTL: 0 mInitializeAlignment: 0 ---- !u!114 &878140411 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 621253541} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1001 &885621924 PrefabInstance: m_ObjectHideFlags: 0 @@ -10866,23 +10536,6 @@ MonoBehaviour: mAlignment_LTR: 513 mAlignmentWasRTL: 0 mInitializeAlignment: 0 ---- !u!114 &889556980 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1347680492} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &894264019 GameObject: m_ObjectHideFlags: 0 @@ -11274,23 +10927,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 909563966} m_CullTransparentMesh: 1 ---- !u!114 &913861675 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 28725820} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &915397018 GameObject: m_ObjectHideFlags: 0 @@ -11570,23 +11206,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: f7db12c4a04bb4249a4916541b6d0e5d, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!114 &930500101 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1559217498} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &932247136 GameObject: m_ObjectHideFlags: 0 @@ -11737,23 +11356,6 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: playerInput: {fileID: 932541948} ---- !u!114 &946999399 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 2083200087} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &951226828 GameObject: m_ObjectHideFlags: 0 @@ -12114,23 +11716,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 993868748} m_CullTransparentMesh: 1 ---- !u!114 &1002167966 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1669280156} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1028956952 GameObject: m_ObjectHideFlags: 0 @@ -13057,23 +12642,6 @@ MonoBehaviour: m_FillOrigin: 0 m_UseSpriteMesh: 0 m_PixelsPerUnitMultiplier: 1 ---- !u!114 &1085979564 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1517266524} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1089279497 GameObject: m_ObjectHideFlags: 0 @@ -13690,23 +13258,6 @@ RectTransform: m_AnchoredPosition: {x: 400, y: 150} m_SizeDelta: {x: 800, y: 1200} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1122701997 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 618614392} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1001 &1127235732 PrefabInstance: m_ObjectHideFlags: 0 @@ -14267,18 +13818,6 @@ MonoBehaviour: m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 m_ReverseArrangement: 0 ---- !u!114 &1138095262 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6504848274583113892, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 532718509} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1138776081 GameObject: m_ObjectHideFlags: 0 @@ -14521,30 +14060,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1162430350 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 4944485236584090101, guid: 8a274db7aa7755b4e8787654931287d1, - type: 3} - m_PrefabInstance: {fileID: 471119987} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &1177858397 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1436411705962800859, guid: 02b942a4ac1a0e047bcc7cf6fc2664e0, - type: 3} - m_PrefabInstance: {fileID: 1440750072} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1186915510 GameObject: m_ObjectHideFlags: 0 @@ -15257,47 +14772,6 @@ MonoBehaviour: mGUI_ShowCallback: 0 mLocalizeTarget: {fileID: 1568404963} mLocalizeTargetName: I2.Loc.LocalizeTarget_TextMeshPro_UGUI ---- !u!114 &1258571401 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1722593142608310948, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 532718509} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &1259724426 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 720380234} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1273198604 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1722593142608310948, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 2142257916} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1283588606 GameObject: m_ObjectHideFlags: 0 @@ -15636,23 +15110,6 @@ CanvasGroup: m_Interactable: 0 m_BlocksRaycasts: 0 m_IgnoreParentGroups: 0 ---- !u!114 &1299534689 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 188698197} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!114 &1299562985 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 5598452497064675529, guid: 02b942a4ac1a0e047bcc7cf6fc2664e0, @@ -16004,52 +15461,6 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 5151cbfb4a7c39c4599fb711d3332cbd, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!114 &1312602307 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 622510712} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1329414378 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1940742647} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1347680492 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 2474312952025664795, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 480190292} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!114 &1351337659 MonoBehaviour: m_ObjectHideFlags: 0 @@ -16188,40 +15599,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 1920, y: 1080} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1366410047 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1605543330} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1367154788 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 514082062} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1371572582 GameObject: m_ObjectHideFlags: 0 @@ -16402,52 +15779,6 @@ MonoBehaviour: backButton: {fileID: 463854110} settingsWindowController: {fileID: 1094166300} offsetEditor: {fileID: 1733771706} ---- !u!114 &1387764614 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1722593142608310948, guid: cc732506675db1f4b9bf9bf0c997748c, - type: 3} - m_PrefabInstance: {fileID: 1421732610} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &1391602527 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1273198604} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1392987254 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1138095262} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1416061603 GameObject: m_ObjectHideFlags: 0 @@ -16524,23 +15855,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1416061603} m_CullTransparentMesh: 1 ---- !u!114 &1419120404 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1531752875} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1001 &1421732610 PrefabInstance: m_ObjectHideFlags: 0 @@ -17890,18 +17204,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1509213223} m_CullTransparentMesh: 1 ---- !u!114 &1517266524 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 4944485236584090101, guid: 8a274db7aa7755b4e8787654931287d1, - type: 3} - m_PrefabInstance: {fileID: 275260319} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1519945291 GameObject: m_ObjectHideFlags: 0 @@ -18170,35 +17472,6 @@ MonoBehaviour: - {fileID: 11400000, guid: 0fcf41ebfb43a9642be661b720007d78, type: 2} tutorialCollection: {fileID: 11400000, guid: b35818759a4d37645b51a8f3ef2fbaa9, type: 2} currentChapter: {fileID: 11400000, guid: f36a8d771b3945a4c9968628b65ac876, type: 2} ---- !u!114 &1531263098 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 233358388} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1531752875 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6504848274583113892, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 805060254} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1534639413 GameObject: m_ObjectHideFlags: 0 @@ -18337,18 +17610,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1534639413} m_CullTransparentMesh: 1 ---- !u!114 &1550667738 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 5643531126894557675, guid: cc732506675db1f4b9bf9bf0c997748c, - type: 3} - m_PrefabInstance: {fileID: 1951870066} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1550903663 GameObject: m_ObjectHideFlags: 0 @@ -18520,18 +17781,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1558137782} m_CullTransparentMesh: 0 ---- !u!114 &1559217498 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 2474312952025664795, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 1609000271} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!114 &1567730149 MonoBehaviour: m_ObjectHideFlags: 0 @@ -18851,23 +18100,6 @@ RectTransform: m_AnchoredPosition: {x: 65, y: 45} m_SizeDelta: {x: 980, y: 551} m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1579769057 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 41005621} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1589396085 GameObject: m_ObjectHideFlags: 0 @@ -18919,52 +18151,6 @@ CanvasGroup: m_Interactable: 1 m_BlocksRaycasts: 1 m_IgnoreParentGroups: 0 ---- !u!114 &1590055873 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1550667738} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1600073186 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1387764614} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 ---- !u!114 &1605543330 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 2474312952025664795, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 2142257916} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1001 &1609000271 PrefabInstance: m_ObjectHideFlags: 0 @@ -19863,18 +19049,6 @@ MonoBehaviour: m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 m_ReverseArrangement: 0 ---- !u!114 &1665326683 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 5643531126894557675, guid: cc732506675db1f4b9bf9bf0c997748c, - type: 3} - m_PrefabInstance: {fileID: 1421732610} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1665548620 GameObject: m_ObjectHideFlags: 0 @@ -19962,18 +19136,6 @@ MonoBehaviour: - {fileID: 2110484866} - {fileID: 1237902644} selectedButton: {fileID: 0} ---- !u!114 &1669280156 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1722593142608310948, guid: cc732506675db1f4b9bf9bf0c997748c, - type: 3} - m_PrefabInstance: {fileID: 1951870066} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1672615610 GameObject: m_ObjectHideFlags: 0 @@ -20509,23 +19671,6 @@ CanvasGroup: m_Interactable: 1 m_BlocksRaycasts: 1 m_IgnoreParentGroups: 0 ---- !u!114 &1714595610 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1875174340} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1718083120 GameObject: m_ObjectHideFlags: 0 @@ -20664,18 +19809,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1718083120} m_CullTransparentMesh: 1 ---- !u!114 &1719155780 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 8415253725780224226, guid: a70c5e7fa5471904a82d9296a7fb70c7, - type: 3} - m_PrefabInstance: {fileID: 885621924} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1724035088 GameObject: m_ObjectHideFlags: 0 @@ -20791,35 +19924,6 @@ MonoBehaviour: descriptionText: {fileID: 860827495} largeOffsetHintText: {fileID: 1250325744} backButton: {fileID: 2050618682} ---- !u!114 &1748514909 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6504848274583113892, guid: cc732506675db1f4b9bf9bf0c997748c, - type: 3} - m_PrefabInstance: {fileID: 1421732610} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &1749097455 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 781326709} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1754184510 GameObject: m_ObjectHideFlags: 0 @@ -21224,18 +20328,6 @@ MonoBehaviour: inputActions: {fileID: -944628639613478452, guid: 6a9b5e1cce519a041b93c008ab19ca4d, type: 3} resetButton: {fileID: 1421732612} ---- !u!114 &1781454272 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 4944485236584090101, guid: 8a274db7aa7755b4e8787654931287d1, - type: 3} - m_PrefabInstance: {fileID: 846975560} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1782838907 GameObject: m_ObjectHideFlags: 0 @@ -21473,30 +20565,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1796831038} m_CullTransparentMesh: 1 ---- !u!114 &1803845447 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6504848274583113892, guid: 14c67a04f94221942b124721a7dfa6fa, - type: 3} - m_PrefabInstance: {fileID: 480190292} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &1817954625 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 4944485236584090101, guid: 8a274db7aa7755b4e8787654931287d1, - type: 3} - m_PrefabInstance: {fileID: 657205184} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1832796522 GameObject: m_ObjectHideFlags: 0 @@ -22106,18 +21174,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1870072470} m_CullTransparentMesh: 1 ---- !u!114 &1875174340 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1436411705962800859, guid: 02b942a4ac1a0e047bcc7cf6fc2664e0, - type: 3} - m_PrefabInstance: {fileID: 717981341} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &1876757692 GameObject: m_ObjectHideFlags: 0 @@ -22619,18 +21675,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1937063703} m_CullTransparentMesh: 1 ---- !u!114 &1940742647 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1154235964385939689, guid: a70c5e7fa5471904a82d9296a7fb70c7, - type: 3} - m_PrefabInstance: {fileID: 885621924} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1001 &1951870066 PrefabInstance: m_ObjectHideFlags: 0 @@ -22952,23 +21996,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1979587995} m_CullTransparentMesh: 1 ---- !u!114 &1995563222 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 438752297} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &1998834541 GameObject: m_ObjectHideFlags: 0 @@ -23227,23 +22254,6 @@ MonoBehaviour: mGUI_ShowCallback: 0 mLocalizeTarget: {fileID: 1868936611} mLocalizeTargetName: I2.Loc.LocalizeTarget_TextMeshPro_UGUI ---- !u!114 &2005330525 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1748514909} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &2005595456 GameObject: m_ObjectHideFlags: 0 @@ -28298,23 +27308,6 @@ ParticleSystem: m_PostInfinity: 2 m_RotationOrder: 4 vectorLabel1_3: W ---- !u!114 &2029832919 -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: 481ab606793a67349be805c13febeba0, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::I2.Loc.LocalizeTarget_TextMeshPro_UGUI - mTarget: {fileID: 1162430350} - mAlignment_RTL: 516 - mAlignment_LTR: 513 - mAlignmentWasRTL: 0 - mInitializeAlignment: 1 --- !u!1 &2050618681 GameObject: m_ObjectHideFlags: 0 @@ -28813,18 +27806,6 @@ MonoBehaviour: Entry: 6 Data: mainCanvasGroup: {fileID: 2066043558} ---- !u!114 &2083200087 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 1890038081481639440, guid: 02b942a4ac1a0e047bcc7cf6fc2664e0, - type: 3} - m_PrefabInstance: {fileID: 717981341} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!1 &2091561605 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleAutoOrient.cs b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleAutoOrient.cs index 9047fcf5..0bd4c92c 100644 --- a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleAutoOrient.cs +++ b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleAutoOrient.cs @@ -29,7 +29,7 @@ namespace Ichni.RhythmGame this.splineRenderer.doubleSided = true; this.splineRenderer.clipFrom = 0; this.splineRenderer.clipTo = 1; - this.splineRenderer.updateMethod = SplineUser.UpdateMethod.Update; + this.splineRenderer.updateMethod = SplineUser.UpdateMethod.LateUpdate; this.meshRenderer.material = renderMaterial; this.splineRenderer.color = Color.white; this.uvRotation = 0f; diff --git a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmodulePathGenerator.cs b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmodulePathGenerator.cs index bc19b37d..40ed2277 100644 --- a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmodulePathGenerator.cs +++ b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmodulePathGenerator.cs @@ -28,7 +28,7 @@ namespace Ichni.RhythmGame this.pathGenerator.doubleSided = true; this.pathGenerator.clipFrom = 0; this.pathGenerator.clipTo = 1; - this.pathGenerator.updateMethod = SplineUser.UpdateMethod.Update; + this.pathGenerator.updateMethod = SplineUser.UpdateMethod.LateUpdate; this.meshRenderer.material = renderMaterial; this.pathGenerator.color = Color.white; this.uvRotation = 90f; diff --git a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleSurface.cs b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleSurface.cs index af00b47d..164801f7 100644 --- a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleSurface.cs +++ b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleSurface.cs @@ -27,7 +27,7 @@ namespace Ichni.RhythmGame this.surface.doubleSided = true; this.surface.clipFrom = 0; this.surface.clipTo = 1; - this.surface.updateMethod = SplineUser.UpdateMethod.Update; + this.surface.updateMethod = SplineUser.UpdateMethod.LateUpdate; this.meshRenderer.material = renderMaterial; this.surface.color = Color.white; this.surface.uvRotation = 90; diff --git a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleTubeGenerator.cs b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleTubeGenerator.cs index 99d5d254..cf88e1cc 100644 --- a/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleTubeGenerator.cs +++ b/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackRendererSubmodule/TrackRendererSubmoduleTubeGenerator.cs @@ -29,7 +29,7 @@ namespace Ichni.RhythmGame this.tubeGenerator.spline = track.trackPathSubmodule.path; this.tubeGenerator.clipFrom = 0; this.tubeGenerator.clipTo = 1; - this.tubeGenerator.updateMethod = SplineUser.UpdateMethod.Update; + this.tubeGenerator.updateMethod = SplineUser.UpdateMethod.LateUpdate; this.meshRenderer.material = renderMaterial; this.tubeGenerator.color = Color.white; this.tubeGenerator.uvRotation = 90; diff --git a/Assets/Scripts/Manager/AudioManager.cs b/Assets/Scripts/Manager/AudioManager.cs new file mode 100644 index 00000000..ff2cbe05 --- /dev/null +++ b/Assets/Scripts/Manager/AudioManager.cs @@ -0,0 +1,10 @@ +using Sirenix.OdinInspector; +using UnityEngine; + +namespace Ichni +{ + public class AudioManager : SLSUtilities.WwiseAssistance.AudioManager + { + + } +} \ No newline at end of file diff --git a/Assets/Scripts/Manager/AudioManager.cs.meta b/Assets/Scripts/Manager/AudioManager.cs.meta new file mode 100644 index 00000000..8db3f207 --- /dev/null +++ b/Assets/Scripts/Manager/AudioManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0aca77fb4d2655046a9ed2ccd2c14321 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/General/StringExtension.cs b/Assets/Scripts/SLSUtilities/General/StringExtension.cs index a32b9731..abd90ea7 100644 --- a/Assets/Scripts/SLSUtilities/General/StringExtension.cs +++ b/Assets/Scripts/SLSUtilities/General/StringExtension.cs @@ -1,4 +1,4 @@ -using I2.Loc; +//using I2.Loc; using UnityEngine; namespace SLSUtilities.General @@ -7,10 +7,10 @@ namespace SLSUtilities.General { public static string Localize(this string original) { - if (LocalizationManager.TryGetTranslation(original, out string translated)) + /*if (LocalizationManager.TryGetTranslation(original, out string translated)) { return translated; - } + }*/ return original; } diff --git a/Assets/Scripts/SLSUtilities/Narrative.meta b/Assets/Scripts/SLSUtilities/Narrative.meta new file mode 100644 index 00000000..620d0c38 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1356e0f522232cc4d8407d114731b30d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base.meta b/Assets/Scripts/SLSUtilities/Narrative/Base.meta new file mode 100644 index 00000000..e903a730 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5d41106686f65de4ab351d68b4889afa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/Data.meta new file mode 100644 index 00000000..a29d92a0 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb73d2cd16307f942b13d90f04e73802 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/CharacterData.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/CharacterData.cs new file mode 100644 index 00000000..10accfb8 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/CharacterData.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; + +namespace SLSUtilities.Narrative +{ + [CreateAssetMenu(fileName = "New Character Data", menuName = "SLSUtilities/Story System/Character Data")] + public class CharacterData : SerializedScriptableObject + { + [TitleGroup("角色档案", "Yarn Spinner 角色视觉与差分配置", Alignment = TitleAlignments.Centered)] + + [BoxGroup("角色档案/基础信息 (Basic Info)")] + [LabelText("角色名称 (Yarn 识别码)")] + [Tooltip("在 C# 逻辑与场景注册中使用的标准唯一英文 ID(例如:'Player' 或 'Guide')。")] + public string nameKey; + + [BoxGroup("角色档案/基础信息 (Basic Info)")] + [LabelText("显示名称 (Display Name)")] + [Tooltip("在 Yarn 对话文本中显示的本地化名称(例如中文:'引导者')。用于将文本说话人匹配到标准英文 ID。")] + public List alias; + + [BoxGroup("角色档案/立绘差分 (Portraits)", centerLabel: true)] + [LabelText("默认立绘 (Default Portrait)")] + [PreviewField(70, ObjectFieldAlignment.Left)] + [Tooltip("当 Yarn 台词没有指定 #mood 标签时,显示的默认角色立绘。")] + public Sprite defaultPortrait; + + [BoxGroup("角色档案/立绘差分 (Portraits)")] + [LabelText("表情差分库 (Mood Expressions)")] + [DictionaryDrawerSettings(KeyLabel = "表情标签 (如 Happy, Sad)", ValueLabel = "对应的立绘 (Sprite)", DisplayMode = DictionaryDisplayOptions.ExpandedFoldout)] + [Tooltip("在此配置各种表情对应的立绘。在 Yarn 中使用 #mood:标签名 来触发。")] + public Dictionary expressions = new Dictionary(); + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/CharacterData.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/CharacterData.cs.meta new file mode 100644 index 00000000..cc4c31ad --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/CharacterData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8ad44d17e97acb747b3b7649aa6d3661 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/KeywordData.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/KeywordData.cs new file mode 100644 index 00000000..a931df92 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/KeywordData.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; +using UnityEngine.Serialization; + +namespace SLSUtilities.Narrative +{ + [CreateAssetMenu(fileName = "New Keyword Data", menuName = "SLSUtilities/Story System/Keyword Data")] + public class KeywordData : SerializedScriptableObject + { + [TitleGroup("关键词档案", "剧情百科中的词条配置", Alignment = TitleAlignments.Centered)] + + [BoxGroup("关键词档案/基础信息 (Basic Info)")] + [LabelText("关键词 (Primary Keyword)")] + [Tooltip("主要的关键词文本,将在台词中被自动识别并高亮。")] + public string keyword; + + [BoxGroup("关键词档案/基础信息 (Basic Info)")] + [LabelText("别名 (Aliases)")] + [Tooltip("该关键词的其他写法或简称,同样会被自动识别。例如:'灵能者'的别名可以是'灵能师'、'Psion'。")] + public List aliases = new List(); + + [BoxGroup("关键词档案/词条内容 (Content)")] + [LabelText("解释文本 (Description)")] + [Tooltip("当玩家悬停时显示的解释内容。如果文本中包含其他已注册的关键词,会自动生成嵌套链接。")] + public string description; + + /// + /// 返回所有可触发该词条的文本(主关键词 + 所有别名)。 + /// + public IEnumerable GetAllTriggerWords() + { + yield return keyword; + foreach (var alias in aliases) + { + if (!string.IsNullOrWhiteSpace(alias)) + { + yield return alias; + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/KeywordData.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/KeywordData.cs.meta new file mode 100644 index 00000000..1cfc3080 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/KeywordData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 088b2e3aa8e7aad43b9a0230097676de \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/NarrativeEntry.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/NarrativeEntry.cs new file mode 100644 index 00000000..64ba70f6 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/NarrativeEntry.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; + +namespace SLSUtilities.Narrative +{ + /// + /// 一个 NarrativeEntry 对应一个“剧情触发源”(如一个 NPC、一个区域、一个道具)。 + /// 它包含一个有序的条件→节点路由列表。 + /// + [CreateAssetMenu(fileName = "NarrativeEntry", menuName = "SLSUtilities/Story System/Narrative Entry")] + public class NarrativeEntry : SerializedScriptableObject + { + [TitleGroup("Identity", "剧情触发源身份标识", Alignment = TitleAlignments.Centered)] + [Required] + [Tooltip("全局唯一标识,对应 NPC 或触发器的 storyId (如 'OldMan', 'Forest_Gate')")] + public string storyId; + + [TitleGroup("Routing Rules", "路由规则表 (从上到下评估,首个满足的生效)", Alignment = TitleAlignments.Centered)] + [ListDrawerSettings(ShowPaging = true, NumberOfItemsPerPage = 10)] + public List routes = new List(); + + [TitleGroup("Fallback", "兜底处理 (当所有路由条件都不满足时播什么)", Alignment = TitleAlignments.Centered)] + [Tooltip("兜底节点(可为空,为空则不播放任何对话)")] + public string fallbackNode; + } + + /// + /// 单条路由规则:描述备注 + 条件列表 + 目标 Yarn 节点。 + /// + [Serializable] + public class NarrativeRoute + { + [LabelText("描述 (仅编辑器备注)")] + [Required] + [Tooltip("例如:'第一次见面'、'已购买提灯后'")] + public string editorNote; + + [LabelText("条件列表 (全部满足才匹配)")] + [ListDrawerSettings(ShowIndexLabels = false)] + public List conditions = new List(); + + [LabelText("目标 Yarn 节点")] + [Required] + [Tooltip("满足上述条件时播放的 Yarn Node 名称 (如 'OldMan_FirstMeet')")] + public string targetNode; + } + + /// + /// 单个变量匹配条件:变量类型 + 变量名 + 比较方式 + 目标值。 + /// + [Serializable] + public class NarrativeCondition + { + public enum ConditionType + { + Bool, + Int, + Float, + String + } + + public enum CompareOp + { + [LabelText("==")] Equal, + [LabelText("!=")] NotEqual, + [LabelText(">")] Greater, + [LabelText(">=")] GreaterOrEqual, + [LabelText("<")] Less, + [LabelText("<=")] LessOrEqual + } + + [HorizontalGroup("Cond", Width = 70)] + [HideLabel] + public ConditionType type = ConditionType.Bool; + + [HorizontalGroup("Cond")] + [HideLabel] + [Required] + [Tooltip("StorySystem 变量名 (如 'has_lantern')")] + public string key; + + [HorizontalGroup("Cond", Width = 60)] + [HideLabel] + public CompareOp op = CompareOp.Equal; + + [HorizontalGroup("Cond")] + [HideLabel] + [Required] + [Tooltip("对比的值,布尔值请填 true/false,其他按相应格式填写")] + public string value; + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/NarrativeEntry.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/NarrativeEntry.cs.meta new file mode 100644 index 00000000..bafd0503 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/NarrativeEntry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7033ff7e5a062be4ca2763804709b342 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/StoryProjectDatabase.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/StoryProjectDatabase.cs new file mode 100644 index 00000000..90aada58 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/StoryProjectDatabase.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace SLSUtilities.Narrative +{ + [CreateAssetMenu(fileName = "Story Project Database", menuName = "SLSUtilities/Story System/Story Project Database")] + public class StoryProjectDatabase : SerializedScriptableObject + { + // 巧妙利用 Odin 的嵌套分组语法:"父分组/子分组" + // 这样就可以让 TitleGroup 作为父节点显示在最上方,而 BoxGroup 嵌套在其中,解决了 Title 被包裹在 Box 里的问题。 + [TitleGroup("全局剧情数据库", "集中管理所有剧情相关数据的注册中心", Alignment = TitleAlignments.Centered)] + + [ListDrawerSettings(ShowIndexLabels = true, ListElementLabelName = "nameKey")] + [BoxGroup("全局剧情数据库/角色档案 (Characters)")] + [LabelText("角色档案列表 (Character Profiles)")] + public List characters = new List(); + + [ListDrawerSettings(ShowIndexLabels = true)] + [BoxGroup("全局剧情数据库/变量数据 (Variables)")] + [LabelText("变量数据组 (Variable Groups)")] + public List variables = new List(); + + [ListDrawerSettings(ShowIndexLabels = true, ListElementLabelName = "keyword")] + [BoxGroup("全局剧情数据库/已注册的资产 (Registered Assets)")] + [LabelText("关键词词条 (Keywords)")] + public List keywords = new List(); + + [ListDrawerSettings(ShowIndexLabels = true, ListElementLabelName = "storyId")] + [BoxGroup("全局剧情数据库/剧情入口 (Narrative Entries)")] + [LabelText("剧情入口路由表 (Narrative Entries)")] + public List narrativeEntries = new List(); + + [TitleGroup("Yarn File Export Settings", "NPC 变量定义文件导出配置", Alignment = TitleAlignments.Centered)] + [FolderPath] + [Required("请指定 Yarn 文件的导出目标文件夹!")] + [Tooltip("生成的 NPC_IDs.yarn 文件的保存目录(建议放在 Yarn 脚本文件夹下)")] + public string exportFolder = "Assets/Story"; + + [Button("生成 NPC_IDs.yarn (Generate)", ButtonSizes.Medium)] + [GUIColor(0.2f, 0.8f, 0.4f)] + public void GenerateNpcIdsYarnFile() + { + if (string.IsNullOrEmpty(exportFolder)) + { + Debug.LogError("[StoryProjectDatabase] 导出失败:未指定有效的导出文件夹路径!"); + return; + } + + if (characters == null || characters.Count == 0) + { + Debug.LogWarning("[StoryProjectDatabase] 角色列表为空,已取消生成。"); + return; + } + + // 确保目标导出目录存在 + if (!System.IO.Directory.Exists(exportFolder)) + { + try + { + System.IO.Directory.CreateDirectory(exportFolder); + } + catch (System.Exception ex) + { + Debug.LogError($"[StoryProjectDatabase] 无法创建目标文件夹 '{exportFolder}': {ex.Message}"); + return; + } + } + + string filePath = System.IO.Path.Combine(exportFolder, "NPC_IDs.yarn"); + try + { + using (System.IO.StreamWriter writer = new System.IO.StreamWriter(filePath, false, System.Text.Encoding.UTF8)) + { + writer.WriteLine("// ==========================================================================="); + writer.WriteLine("// 自动生成的 NPC 英文标准名 ID 变量定义文件。"); + writer.WriteLine("// 供 VS Code Yarn Spinner 插件进行命令变量补全(例如:输入 $NPC_ 触发提示)。"); + writer.WriteLine("// 警告:该文件为程序自动生成,请勿在此文件中手动编辑或添加内容。"); + writer.WriteLine("// ==========================================================================="); + writer.WriteLine(); + + // 必须将 declare 声明置于有效的 Node 结构内,防止编译器在解析无 Node 的文件时抛出 Token 识别错误(token recognition error at: '<') + writer.WriteLine("title: NPC_IDs"); + writer.WriteLine("---"); + + foreach (var charData in characters) + { + if (charData == null || string.IsNullOrWhiteSpace(charData.nameKey)) continue; + + string trimmedId = charData.nameKey.Trim(); + // 生成格式如:<> + writer.WriteLine($"<>"); + } + + writer.WriteLine("==="); + } + +#if UNITY_EDITOR + // 刷新 AssetDatabase,让 Unity 编辑器立刻加载生成的 .yarn 资源 + UnityEditor.AssetDatabase.Refresh(); +#endif + Debug.Log($"[StoryProjectDatabase] 成功生成/更新 NPC 声明文件: '{filePath}'"); + } + catch (System.Exception ex) + { + Debug.LogError($"[StoryProjectDatabase] 导出 NPC_IDs.yarn 失败: {ex.Message}"); + } + } + + [Button("自动扫描并注册数据 (Auto-Scan Directory)", ButtonSizes.Large, Icon = SdfIconType.Search)] + [GUIColor(0.4f, 0.8f, 1f)] + [PropertyTooltip("自动在当前数据库所在的文件夹(及其子文件夹)中寻找所有的 CharacterData、VariableData、KeywordData 和 NarrativeEntry,并自动填入上方的列表中。")] + public void AutoPopulate() + { +#if UNITY_EDITOR + // 获取当前 Database 资产所在的目录路径 + string dbPath = AssetDatabase.GetAssetPath(this); + if (string.IsNullOrEmpty(dbPath)) + { + Debug.LogWarning("[StorySystem] 请先将数据库资产 (Database) 保存到项目目录中。"); + return; + } + string searchDirectory = System.IO.Path.GetDirectoryName(dbPath); + + // 搜索 CharacterData + string[] charGuids = AssetDatabase.FindAssets($"t:{nameof(CharacterData)}", new[] { searchDirectory }); + characters.Clear(); + foreach (var guid in charGuids) + { + var assetPath = AssetDatabase.GUIDToAssetPath(guid); + var charData = AssetDatabase.LoadAssetAtPath(assetPath); + if (charData != null && !characters.Contains(charData)) + characters.Add(charData); + } + + // 搜索 VariableData + string[] varGuids = AssetDatabase.FindAssets($"t:{nameof(VariableData)}", new[] { searchDirectory }); + variables.Clear(); + foreach (var guid in varGuids) + { + var assetPath = AssetDatabase.GUIDToAssetPath(guid); + var varData = AssetDatabase.LoadAssetAtPath(assetPath); + if (varData != null && !variables.Contains(varData)) + variables.Add(varData); + } + + // 搜索 KeywordData + string[] kwGuids = AssetDatabase.FindAssets($"t:{nameof(KeywordData)}", new[] { searchDirectory }); + keywords.Clear(); + foreach (var guid in kwGuids) + { + var assetPath = AssetDatabase.GUIDToAssetPath(guid); + var kwData = AssetDatabase.LoadAssetAtPath(assetPath); + if (kwData != null && !keywords.Contains(kwData)) + keywords.Add(kwData); + } + + // 搜索 NarrativeEntry + string[] entryGuids = AssetDatabase.FindAssets($"t:{nameof(NarrativeEntry)}", new[] { searchDirectory }); + narrativeEntries.Clear(); + foreach (var guid in entryGuids) + { + var assetPath = AssetDatabase.GUIDToAssetPath(guid); + var entryData = AssetDatabase.LoadAssetAtPath(assetPath); + if (entryData != null && !narrativeEntries.Contains(entryData)) + narrativeEntries.Add(entryData); + } + + EditorUtility.SetDirty(this); + AssetDatabase.SaveAssets(); + Debug.Log($"[StorySystem] 自动扫描完成!共注册了 {characters.Count} 个角色档案、{variables.Count} 个变量数据组、{keywords.Count} 个关键词词条 和 {narrativeEntries.Count} 个剧情入口路由表。"); +#else + Debug.LogWarning("自动扫描 (AutoPopulate) 只能在 Unity 编辑器环境下运行。"); +#endif + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/StoryProjectDatabase.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/StoryProjectDatabase.cs.meta new file mode 100644 index 00000000..ca31792c --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/StoryProjectDatabase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7e578280b10e24b40859914cec13cf78 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/VariableData.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/VariableData.cs new file mode 100644 index 00000000..6f1b408c --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/VariableData.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; + +namespace SLSUtilities.Narrative +{ + [CreateAssetMenu(fileName = "StoryVariableData", menuName = "SLSUtilities/Story System/Variable Data")] + public class VariableData : SerializedScriptableObject + { + [Title("变量数据", titleAlignment: TitleAlignments.Centered)] + + [Searchable] + public Dictionary boolVariables = new Dictionary(); + + [Searchable] + public Dictionary intVariables = new Dictionary(); + + [Searchable] + public Dictionary floatVariables = new Dictionary(); + + [Searchable] + public Dictionary stringVariables = new Dictionary(); + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/Data/VariableData.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/VariableData.cs.meta new file mode 100644 index 00000000..b7737a08 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/Data/VariableData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f93ca4ba1ee871b4b944a077b0ec2fab \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeConditionEvaluator.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeConditionEvaluator.cs new file mode 100644 index 00000000..33ba4680 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeConditionEvaluator.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace SLSUtilities.Narrative +{ + /// + /// 条件匹配评估工具类。 + /// 读取并比对 StorySystem.Variables 中的运行时状态,支持多种类型的变量与运算符。 + /// + public static class NarrativeConditionEvaluator + { + /// + /// 评估一系列条件是否全部满足 (AND 关系)。 + /// + public static bool Evaluate(List conditions) + { + if (conditions == null || conditions.Count == 0) + return true; // 无条件默认满足 + + if (StorySystem.Variables == null) + { + Debug.LogWarning("[StorySystem] 评估条件失败:StorySystem.Variables 尚未初始化。"); + return false; + } + + foreach (var cond in conditions) + { + if (string.IsNullOrEmpty(cond.key)) + { + Debug.LogWarning("[StorySystem] 条件配置错误:变量 key 为空。"); + return false; + } + + if (!EvaluateCondition(cond)) + return false; // 任何一个条件不满足,直接返回 false + } + + return true; + } + + private static bool EvaluateCondition(NarrativeCondition cond) + { + switch (cond.type) + { + case NarrativeCondition.ConditionType.Bool: + return EvaluateBool(cond); + case NarrativeCondition.ConditionType.Int: + return EvaluateInt(cond); + case NarrativeCondition.ConditionType.Float: + return EvaluateFloat(cond); + case NarrativeCondition.ConditionType.String: + return EvaluateString(cond); + default: + Debug.LogWarning($"[StorySystem] 未知的条件类型: {cond.type}"); + return false; + } + } + + private static bool EvaluateBool(NarrativeCondition cond) + { + bool currentVal = false; + if (!StorySystem.Variables.boolVariables.TryGetValue(cond.key, out currentVal)) + { + Debug.LogWarning($"[StorySystem] 变量未找到: Bool '{cond.key}',使用默认值 false 匹配。"); + } + + if (!bool.TryParse(cond.value, out bool targetVal)) + { + Debug.LogError($"[StorySystem] 无法将目标值 '{cond.value}' 解析为 Bool。条件键: {cond.key}"); + return false; + } + + switch (cond.op) + { + case NarrativeCondition.CompareOp.Equal: + return currentVal == targetVal; + case NarrativeCondition.CompareOp.NotEqual: + return currentVal != targetVal; + default: + Debug.LogWarning($"[StorySystem] 布尔类型不支持比较运算符: {cond.op},必须为 Equal 或 NotEqual。"); + return false; + } + } + + private static bool EvaluateInt(NarrativeCondition cond) + { + int currentVal = 0; + if (!StorySystem.Variables.intVariables.TryGetValue(cond.key, out currentVal)) + { + Debug.LogWarning($"[StorySystem] 变量未找到: Int '{cond.key}',使用默认值 0 匹配。"); + } + + if (!int.TryParse(cond.value, out int targetVal)) + { + Debug.LogError($"[StorySystem] 无法将目标值 '{cond.value}' 解析为 Int。条件键: {cond.key}"); + return false; + } + + switch (cond.op) + { + case NarrativeCondition.CompareOp.Equal: + return currentVal == targetVal; + case NarrativeCondition.CompareOp.NotEqual: + return currentVal != targetVal; + case NarrativeCondition.CompareOp.Greater: + return currentVal > targetVal; + case NarrativeCondition.CompareOp.GreaterOrEqual: + return currentVal >= targetVal; + case NarrativeCondition.CompareOp.Less: + return currentVal < targetVal; + case NarrativeCondition.CompareOp.LessOrEqual: + return currentVal <= targetVal; + default: + return false; + } + } + + private static bool EvaluateFloat(NarrativeCondition cond) + { + float currentVal = 0f; + if (!StorySystem.Variables.floatVariables.TryGetValue(cond.key, out currentVal)) + { + Debug.LogWarning($"[StorySystem] 变量未找到: Float '{cond.key}',使用默认值 0.0 匹配。"); + } + + if (!float.TryParse(cond.value, out float targetVal)) + { + Debug.LogError($"[StorySystem] 无法将目标值 '{cond.value}' 解析为 Float。条件键: {cond.key}"); + return false; + } + + switch (cond.op) + { + case NarrativeCondition.CompareOp.Equal: + return Mathf.Approximately(currentVal, targetVal); + case NarrativeCondition.CompareOp.NotEqual: + return !Mathf.Approximately(currentVal, targetVal); + case NarrativeCondition.CompareOp.Greater: + return currentVal > targetVal; + case NarrativeCondition.CompareOp.GreaterOrEqual: + return currentVal >= targetVal; + case NarrativeCondition.CompareOp.Less: + return currentVal < targetVal; + case NarrativeCondition.CompareOp.LessOrEqual: + return currentVal <= targetVal; + default: + return false; + } + } + + private static bool EvaluateString(NarrativeCondition cond) + { + string currentVal = string.Empty; + if (!StorySystem.Variables.stringVariables.TryGetValue(cond.key, out currentVal)) + { + Debug.LogWarning($"[StorySystem] 变量未找到: String '{cond.key}',使用默认空字符串匹配。"); + } + + string targetVal = cond.value ?? string.Empty; + + switch (cond.op) + { + case NarrativeCondition.CompareOp.Equal: + return currentVal == targetVal; + case NarrativeCondition.CompareOp.NotEqual: + return currentVal != targetVal; + default: + Debug.LogWarning($"[StorySystem] 字符串类型不支持比较运算符: {cond.op},必须为 Equal 或 NotEqual。"); + return false; + } + } + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeConditionEvaluator.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeConditionEvaluator.cs.meta new file mode 100644 index 00000000..54a3fa42 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeConditionEvaluator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1021ec61ce2442d49bb15ecfe6dd73e9 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeTrigger.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeTrigger.cs new file mode 100644 index 00000000..e8d165c1 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeTrigger.cs @@ -0,0 +1,75 @@ +using Sirenix.OdinInspector; +using UnityEngine; + +namespace SLSUtilities.Narrative +{ + /// + /// 通用剧情触发器基类。 + /// 仅负责管理触发状态(oneShot, hasFired)与全局故事 ID(storyId)。 + /// 具体触发的时机与条件由派生类(子类)自行重写并定义。 + /// + public abstract class NarrativeTrigger : MonoBehaviour + { + /// + /// 全局静态触发事件。当任何剧情触发器被激活时触发。 + /// 用于彻底解耦 SLSUtilities 核心程序集与 MainGame 层的 StoryDirector。 + /// + public static System.Action OnNarrativeTriggerFired; + + [TitleGroup("Trigger Settings", "剧情触发器核心配置", Alignment = TitleAlignments.Centered)] + + [SerializeField] + [Required] + [Tooltip("要触发的 NarrativeEntry 的 storyId")] + protected string storyId; + + [SerializeField] + [Tooltip("是否仅能触发一次")] + protected bool oneShot = true; + + [ShowInInspector] + [ReadOnly] + [Tooltip("该触发器当前是否已经激活过")] + protected bool hasFired; + + /// 获取触发的目标故事 ID。 + public string StoryId => storyId; + + /// 是否是一次性触发器。 + public bool OneShot => oneShot; + + /// 该触发器是否已经激活过。 + public bool HasFired => hasFired; + + /// + /// 激活触发器,通知订阅者启动剧情。 + /// + [Button("测试触发 (Fire)", ButtonSizes.Small)] + [GUIColor(0.3f, 0.8f, 1f)] + public virtual void Fire() + { + if (oneShot && hasFired) + { + return; + } + + if (string.IsNullOrEmpty(storyId)) + { + Debug.LogWarning($"[NarrativeTrigger] {gameObject.name} 触发失败:未配置 storyId。"); + return; + } + + hasFired = true; + OnNarrativeTriggerFired?.Invoke(storyId); + } + + /// + /// 重置触发状态(允许在 oneShot 模式下重新触发)。 + /// + [Button("重置激活状态 (Reset)", ButtonSizes.Small)] + public virtual void ResetTrigger() + { + hasFired = false; + } + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeTrigger.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeTrigger.cs.meta new file mode 100644 index 00000000..d4ea2beb --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/NarrativeTrigger.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c9bc93394b40fd74c8df5412cb2e8992 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs new file mode 100644 index 00000000..def9d75d --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using Sirenix.OdinInspector; +using SLSUtilities.General; +using UnityEngine; + +namespace SLSUtilities.Narrative +{ + public partial class StorySystem : Singleton + { + public StoryProjectDatabase database; + + public static VariableCollection Variables; + + protected override void Awake() + { + Initialize(true); + LoadVariables(); + } + } + + public partial class StorySystem + { + public static StoryProjectDatabase Database => instance.database; + } + + public partial class StorySystem + { + private static string SavePath => Application.persistentDataPath + "/Story/"; + + public class VariableCollection + { + public Dictionary boolVariables = new Dictionary(); + + public Dictionary intVariables = new Dictionary(); + + public Dictionary floatVariables = new Dictionary(); + + public Dictionary stringVariables = new Dictionary(); + + public void LoadFromData() + { + List variableDataList = Database.variables; + foreach (VariableData variableData in variableDataList) + { + foreach (KeyValuePair boolVar in variableData.boolVariables) + { + if(!boolVariables.TryAdd(boolVar.Key, boolVar.Value)) + { + Debug.LogWarning($"[StorySystem] 变量加载警告:布尔变量 '{boolVar.Key}' 已存在,跳过重复项。"); + } + } + + foreach (KeyValuePair intVar in variableData.intVariables) + { + if(!intVariables.TryAdd(intVar.Key, intVar.Value)) + { + Debug.LogWarning($"[StorySystem] 变量加载警告:整数变量 '{intVar.Key}' 已存在,跳过重复项。"); + } + } + + foreach (KeyValuePair floatVar in variableData.floatVariables) + { + if(!floatVariables.TryAdd(floatVar.Key, floatVar.Value)) + { + Debug.LogWarning($"[StorySystem] 变量加载警告:浮点变量 '{floatVar.Key}' 已存在,跳过重复项。"); + } + } + + foreach (KeyValuePair stringVar in variableData.stringVariables) + { + if (!stringVariables.TryAdd(stringVar.Key, stringVar.Value)) + { + Debug.LogWarning($"[StorySystem] 变量加载警告:字符串变量 '{stringVar.Key}' 已存在,跳过重复项。"); + } + } + } + } + } + + [Button("保存变量数据 (Save Variables)", ButtonSizes.Small, Icon = SdfIconType.Save)] + public void SaveVariables() + { + string variablesSavePath = SavePath + "variables.json"; + ES3.Save("Variables", Variables, variablesSavePath); + } + + public void LoadVariables() + { + string variablesSavePath = SavePath + "variables.json"; + if (!ES3.FileExists(variablesSavePath)) + { + Variables = new VariableCollection(); + Variables.LoadFromData(); + ES3.Save("Variables", Variables, variablesSavePath); + } + else + { + Variables = ES3.Load("Variables", variablesSavePath); + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs.meta new file mode 100644 index 00000000..30d3593c --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bc7844de67aeb4b47b1eef29edaec8eb \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs b/Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs new file mode 100644 index 00000000..48b43ee5 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using Yarn.Unity; + +namespace SLSUtilities.Narrative +{ + public static partial class YarnFunctions + { + [YarnCommand("set_bool")] + public static void Yarn_SetBool(string key, bool value) + { + StorySystem.Variables.boolVariables[key] = value; + } + + [YarnFunction("get_bool")] + public static bool Yarn_GetBool(string key) + { + return StorySystem.Variables.boolVariables.GetValueOrDefault(key, false); + } + + [YarnCommand("set_int")] + public static void Yarn_SetInt(string key, int value) + { + StorySystem.Variables.intVariables[key] = value; + } + + [YarnCommand("modify_int")] + public static void Yarn_ModifyInt(string key, int modification) + { + int currentValue = StorySystem.Variables.intVariables.GetValueOrDefault(key, 0); + StorySystem.Variables.intVariables[key] = currentValue + modification; + } + + [YarnFunction("get_int")] + public static int Yarn_GetInt(string key) + { + return StorySystem.Variables.intVariables.GetValueOrDefault(key, 0); + } + + [YarnCommand("set_float")] + public static void Yarn_SetFloat(string key, float value) + { + StorySystem.Variables.floatVariables[key] = value; + } + + [YarnCommand("modify_float")] + public static void Yarn_ModifyFloat(string key, float modification) + { + float currentValue = StorySystem.Variables.floatVariables.GetValueOrDefault(key, 0f); + StorySystem.Variables.floatVariables[key] = currentValue + modification; + } + + [YarnFunction("get_float")] + public static float Yarn_GetFloat(string key) + { + return StorySystem.Variables.floatVariables.GetValueOrDefault(key, 0f); + } + + [YarnCommand("set_string")] + public static void Yarn_SetString(string key, string value) + { + StorySystem.Variables.stringVariables[key] = value; + } + + [YarnFunction("get_string")] + public static string Yarn_GetString(string key) + { + return StorySystem.Variables.stringVariables.GetValueOrDefault(key, ""); + } + } + + public static partial class YarnFunctions + { + [YarnCommand("log")] + public static void Log(string message, string logType) + { + logType = logType.ToLower(); + + switch (logType) + { + case "info": + UnityEngine.Debug.Log(message); + break; + case "warning": + UnityEngine.Debug.LogWarning(message); + break; + case "error": + UnityEngine.Debug.LogError(message); + break; + default: + UnityEngine.Debug.Log(message); + break; + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs.meta new file mode 100644 index 00000000..cb67d7f9 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fd615694e3d8cc842a6c75bc3956a926 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Editor.meta b/Assets/Scripts/SLSUtilities/Narrative/Editor.meta new file mode 100644 index 00000000..a6074257 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b5e90c1f5a684a4793d38b5a7f11c13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/Editor/AdvancedPresenterEditors.cs b/Assets/Scripts/SLSUtilities/Narrative/Editor/AdvancedPresenterEditors.cs new file mode 100644 index 00000000..62f75864 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Editor/AdvancedPresenterEditors.cs @@ -0,0 +1,32 @@ +using SLSUtilities.Narrative.UI; +using UnityEditor; +using Yarn.Unity.Editor; + +namespace SLSUtilities.Narrative.Editor +{ + /// + /// AdvancedLinePresenter 的自定义 Inspector。 + /// 继承自 Yarn 的 YarnEditor,完整复现 LinePresenter 的 + /// [ShowIf]、[Group]、[MustNotBeNull] 等属性驱动的显示效果。 + /// + [CanEditMultipleObjects] + [CustomEditor(typeof(AdvancedLinePresenter))] + public class AdvancedLinePresenterEditor : YarnEditor { } + + /// + /// AdvancedLineAdvancer 的自定义 Inspector。 + /// 继承自 Yarn 的 YarnEditor,完整复现 LineAdvancer 的 + /// InputMode 条件显示等效果。 + /// + [CanEditMultipleObjects] + [CustomEditor(typeof(AdvancedLineAdvancer))] + public class AdvancedLineAdvancerEditor : YarnEditor { } + + /// + /// AdvancedOptionsPresenter 的自定义 Inspector。 + /// 完整复现 OptionsPresenter 的属性驱动效果。 + /// + [CanEditMultipleObjects] + [CustomEditor(typeof(AdvancedOptionsPresenter))] + public class AdvancedOptionsPresenterEditor : YarnEditor { } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Editor/AdvancedPresenterEditors.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Editor/AdvancedPresenterEditors.cs.meta new file mode 100644 index 00000000..77e0ee5d --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Editor/AdvancedPresenterEditors.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5a5fb6371856a1c4dad725a24657bb10 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Editor/SLSUtilities.Narrative.Editor.asmdef b/Assets/Scripts/SLSUtilities/Narrative/Editor/SLSUtilities.Narrative.Editor.asmdef new file mode 100644 index 00000000..ae578a8b --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Editor/SLSUtilities.Narrative.Editor.asmdef @@ -0,0 +1,21 @@ +{ + "name": "SLSUtilities.StorySystem.Editor", + "rootNamespace": "", + "references": [ + "GUID:43bb4e992d9b32b4bbb25402b41e80a0", + "GUID:22a86856172c06146a539eeb9c9c67f5", + "GUID:34aa492b82754644eac2f903cd496268", + "GUID:3a299c53e4c683b4eb4a04c9ad9e648f" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Editor/SLSUtilities.Narrative.Editor.asmdef.meta b/Assets/Scripts/SLSUtilities/Narrative/Editor/SLSUtilities.Narrative.Editor.asmdef.meta new file mode 100644 index 00000000..273d1c57 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Editor/SLSUtilities.Narrative.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1f17ca624957b754baae4c01af60c96e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs b/Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs new file mode 100644 index 00000000..49d78067 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; +using Newtonsoft.Json.Linq; + +namespace SLSUtilities.Narrative.Editor +{ + /// + /// Yarn Spinner 自动 YSLS 合并器。 + /// 自动将来自不同程序集(Assembly-CSharp、SLSUtilities等)生成的多个 YSLS 声明文件 + /// 合并成一个单一的 combined.ysls.json 文件,输出到项目根目录下, + /// 从而完美解决 VS Code 中 Yarn Spinner 插件只能同时加载一个 definitions 文件的限制! + /// + public static class YslsMerger + { + [MenuItem("Tools/Yarn Spinner/Merge YSLS Files")] + public static void MergeYslsFiles() + { + string packagePath = Path.Combine(Directory.GetCurrentDirectory(), "ProjectSettings", "Packages", "dev.yarnspinner"); + if (!Directory.Exists(packagePath)) + { + return; + } + + string combinedFileName = "combined.ysls.json"; + string outputRootPath = Path.Combine(packagePath, combinedFileName); + + try + { + // 搜索所有以 -generated.ysls.json 结尾的声明文件 + string[] files = Directory.GetFiles(packagePath, "*-generated.ysls.json"); + if (files.Length == 0) + { + return; + } + + JArray allCommands = new JArray(); + JArray allFunctions = new JArray(); + int version = 2; + + foreach (string file in files) + { + if (Path.GetFileName(file) == combinedFileName) continue; + + string json = File.ReadAllText(file); + if (string.IsNullOrEmpty(json)) continue; + + JObject root = JObject.Parse(json); + + if (root.TryGetValue("version", out JToken vToken)) + { + version = Mathf.Max(version, vToken.Value()); + } + + if (root.TryGetValue("commands", out JToken cToken) && cToken is JArray commandsArray) + { + foreach (var cmd in commandsArray) + { + allCommands.Add(cmd); + } + } + + if (root.TryGetValue("functions", out JToken fToken) && fToken is JArray functionsArray) + { + foreach (var func in functionsArray) + { + allFunctions.Add(func); + } + } + } + + // 组装合并后的 JObject + JObject combined = new JObject + { + ["version"] = version, + ["commands"] = allCommands, + ["functions"] = allFunctions + }; + + // 写入项目根目录 + File.WriteAllText(outputRootPath, combined.ToString(Newtonsoft.Json.Formatting.Indented)); + Debug.Log($"[YSLS Merger] 成功将 {files.Length} 个 YSLS 声明文件合并至: '{outputRootPath}'。现在您可以在 VS Code 中一键加载此合并文件!"); + } + catch (System.Exception ex) + { + // 自动合并失败通常发生在初始无缓存时,作警告处理,不中断编辑体验 + Debug.LogWarning($"[YSLS Merger] 自动合并 YSLS 文件失败(这通常是由于 Yarn Spinner 尚未生成初始的 ysls 缓存文件导致的,属于正常现象):{ex.Message}"); + } + } + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs.meta new file mode 100644 index 00000000..f94a09c5 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7178a694f7924194ea3b787839ddd162 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/Prefabs.meta b/Assets/Scripts/SLSUtilities/Narrative/Prefabs.meta new file mode 100644 index 00000000..36ed969a --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8704ac67b33279943a43abe2a294147a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/Prefabs/TooltipPanel.prefab b/Assets/Scripts/SLSUtilities/Narrative/Prefabs/TooltipPanel.prefab new file mode 100644 index 00000000..e74cdbdd --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Prefabs/TooltipPanel.prefab @@ -0,0 +1,857 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &822921496015337489 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2451722883515327516} + - component: {fileID: 5821298512207332447} + - component: {fileID: 8003291120468737892} + - component: {fileID: 8538959725662554642} + m_Layer: 5 + m_Name: PinIndicatorNoTitle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2451722883515327516 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 822921496015337489} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1735022670865247660} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -10, y: -10} + m_SizeDelta: {x: 15, y: 15} + m_Pivot: {x: 0.5, y: 0.49999997} +--- !u!222 &5821298512207332447 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 822921496015337489} + m_CullTransparentMesh: 1 +--- !u!114 &8003291120468737892 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 822921496015337489} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: c6dee682f807c194aba431fb54cc6d7d, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &8538959725662554642 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 822921496015337489} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!1 &919898932274239217 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8932679771255679868} + - component: {fileID: 2302293284727336602} + - component: {fileID: 2108095605505189335} + m_Layer: 5 + m_Name: TitleText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8932679771255679868 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 919898932274239217} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4204169304323323032} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 10, y: 0} + m_SizeDelta: {x: -75, y: 25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2302293284727336602 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 919898932274239217} + m_CullTransparentMesh: 1 +--- !u!114 &2108095605505189335 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 919898932274239217} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Title + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2} + m_sharedMaterial: {fileID: 5688540820414838853, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 24 + m_fontSizeBase: 24 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 1 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &1288486415013319065 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5727255906185222562} + - component: {fileID: 2084483938339261636} + - component: {fileID: 1815344004216971213} + m_Layer: 5 + m_Name: PinIndicator + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5727255906185222562 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1288486415013319065} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4204169304323323032} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: -15, y: 0} + m_SizeDelta: {x: 25, y: 25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2084483938339261636 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1288486415013319065} + m_CullTransparentMesh: 1 +--- !u!114 &1815344004216971213 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1288486415013319065} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: c6dee682f807c194aba431fb54cc6d7d, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &1770448389014005609 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4204169304323323032} + m_Layer: 5 + m_Name: TitleBar + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4204169304323323032 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1770448389014005609} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 4664076838088352918} + - {fileID: 8932679771255679868} + - {fileID: 259397633913720669} + - {fileID: 5727255906185222562} + m_Father: {fileID: 332895709820465634} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 300, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &2304233746971670081 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4664076838088352918} + - component: {fileID: 1051253791116523245} + - component: {fileID: 5038012897209242806} + m_Layer: 5 + m_Name: icon + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4664076838088352918 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2304233746971670081} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4204169304323323032} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 25, y: 0} + m_SizeDelta: {x: 25, y: 25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1051253791116523245 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2304233746971670081} + m_CullTransparentMesh: 1 +--- !u!114 &5038012897209242806 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2304233746971670081} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: b72c64868a74dd04e9b605a346744fb5, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &3156910686184609508 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 332895709820465634} + - component: {fileID: 945386332789889429} + - component: {fileID: 7828897958224438855} + - component: {fileID: 8391361806937792526} + - component: {fileID: 5156175450215433438} + - component: {fileID: 7593135083786652588} + m_Layer: 5 + m_Name: TooltipPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &332895709820465634 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3156910686184609508} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 4204169304323323032} + - {fileID: 1735022670865247660} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &945386332789889429 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3156910686184609508} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b9480658e91a4d548a18e05958b5605f, type: 3} + m_Name: + m_EditorClassIdentifier: SLSUtilities.StorySystem::SLSUtilities.StorySystem.UI.KeywordPanel + titleBarContainer: {fileID: 4204169304323323032} + titleText: {fileID: 2108095605505189335} + iconImage: {fileID: 5038012897209242806} + titlePin: {fileID: 1288486415013319065} + descriptionContainer: {fileID: 1735022670865247660} + descriptionText: {fileID: 284395028850057396} + descriptionPin: {fileID: 822921496015337489} +--- !u!114 &7828897958224438855 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3156910686184609508} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 1 + m_Spacing: 0 + m_ChildForceExpandWidth: 0 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 0 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!222 &8391361806937792526 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3156910686184609508} + m_CullTransparentMesh: 1 +--- !u!114 &5156175450215433438 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3156910686184609508} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 0.25, g: 0.25, b: 0.25, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: cca3ca807d25c1844b79eeb77e944d05, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 8 +--- !u!114 &7593135083786652588 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3156910686184609508} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter + m_HorizontalFit: 2 + m_VerticalFit: 1 +--- !u!1 &3804837773971309440 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1735022670865247660} + - component: {fileID: 7312414472714823656} + - component: {fileID: 5296572890931267466} + - component: {fileID: 8110114097174124860} + m_Layer: 5 + m_Name: Description + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1735022670865247660 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3804837773971309440} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 789625586990900961} + - {fileID: 2451722883515327516} + m_Father: {fileID: 332895709820465634} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 150, y: -50} + m_SizeDelta: {x: 300, y: 40} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7312414472714823656 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3804837773971309440} + m_CullTransparentMesh: 1 +--- !u!114 &5296572890931267466 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3804837773971309440} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement + m_IgnoreLayout: 0 + m_MinWidth: 300 + m_MinHeight: 40 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!114 &8110114097174124860 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3804837773971309440} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter + m_HorizontalFit: 2 + m_VerticalFit: 0 +--- !u!1 &6532964554051255018 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 789625586990900961} + - component: {fileID: 4476208540601885936} + - component: {fileID: 284395028850057396} + m_Layer: 5 + m_Name: Description + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &789625586990900961 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6532964554051255018} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1735022670865247660} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4476208540601885936 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6532964554051255018} + m_CullTransparentMesh: 1 +--- !u!114 &284395028850057396 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6532964554051255018} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Description + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2} + m_sharedMaterial: {fileID: 5688540820414838853, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 24 + m_fontSizeBase: 24 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &6789644190267702728 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 259397633913720669} + - component: {fileID: 5309614983325892601} + - component: {fileID: 1610999632492459883} + m_Layer: 5 + m_Name: Separator + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &259397633913720669 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6789644190267702728} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 4204169304323323032} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: -15} + m_SizeDelta: {x: 0, y: 1} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5309614983325892601 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6789644190267702728} + m_CullTransparentMesh: 1 +--- !u!114 &1610999632492459883 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6789644190267702728} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 diff --git a/Assets/Scripts/SLSUtilities/Narrative/Prefabs/TooltipPanel.prefab.meta b/Assets/Scripts/SLSUtilities/Narrative/Prefabs/TooltipPanel.prefab.meta new file mode 100644 index 00000000..9aa8c69f --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/Prefabs/TooltipPanel.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dc9a7ff16d23a2c41a0152ce105060be +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/SLSUtilities.Narrative.asmdef b/Assets/Scripts/SLSUtilities/Narrative/SLSUtilities.Narrative.asmdef new file mode 100644 index 00000000..315ba275 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/SLSUtilities.Narrative.asmdef @@ -0,0 +1,20 @@ +{ + "name": "SLSUtilities.Narrative", + "rootNamespace": "", + "references": [ + "GUID:43bb4e992d9b32b4bbb25402b41e80a0", + "GUID:34aa492b82754644eac2f903cd496268", + "GUID:6055be8ebefd69e48b49212b09b47b2f", + "GUID:75469ad4d38634e559750d17036d5f7c", + "GUID:cfcd2ce455f8d1944942cdd919ecaa60" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/SLSUtilities.Narrative.asmdef.meta b/Assets/Scripts/SLSUtilities/Narrative/SLSUtilities.Narrative.asmdef.meta new file mode 100644 index 00000000..43fae5df --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/SLSUtilities.Narrative.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 22a86856172c06146a539eeb9c9c67f5 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI.meta b/Assets/Scripts/SLSUtilities/Narrative/UI.meta new file mode 100644 index 00000000..d756b272 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 46e87422871a96e42be74af6d585b634 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLineAdvancer.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLineAdvancer.cs new file mode 100644 index 00000000..08620191 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLineAdvancer.cs @@ -0,0 +1,26 @@ +using Sirenix.OdinInspector; +using UnityEngine; +using Yarn.Unity; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 高级输入控制器,继承自官方 LineAdvancer。 + /// 在保留所有原生输入处理逻辑的基础上, + /// 当关键词 Tooltip 窗口处于打开状态时,阻断本帧的台词推进输入。 + /// + public class AdvancedLineAdvancer : LineAdvancer + { + protected override void RequestLineHurryUpInternal() + { + if (KeywordTooltipUI.IsBlockingDialogueInput) return; + base.RequestLineHurryUpInternal(); + } + + public override void RequestNextLine() + { + if (KeywordTooltipUI.IsBlockingDialogueInput) return; + base.RequestNextLine(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLineAdvancer.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLineAdvancer.cs.meta new file mode 100644 index 00000000..cb05bd64 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLineAdvancer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7c3b5f02f9e109140b36d9e3bad02271 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLinePresenter.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLinePresenter.cs new file mode 100644 index 00000000..01684e4a --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLinePresenter.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine; +using UnityEngine.UI; +using Yarn.Unity; +using Yarn.Markup; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 高级台词展现层,继承自官方 LinePresenter。 + /// 在保留所有原生功能(打字机、淡入淡出、LineAdvancer 状态机)的基础上, + /// 扩展了角色立绘/头像切换、关键词高亮与悬停百科等功能。 + /// + public class AdvancedLinePresenter : LinePresenter + { + [TitleGroup("立绘系统 (Portrait System)", Alignment = TitleAlignments.Centered)] + + [BoxGroup("立绘系统 (Portrait System)/UI 引用")] + [Tooltip("用于显示角色立绘/头像的 Image 组件")] + [SerializeField] private Image portraitImage; + + [BoxGroup("立绘系统 (Portrait System)/UI 引用")] + [Tooltip("立绘的容器节点。当没有立绘时整体隐藏,避免空白区域占位")] + [SerializeField] private GameObject portraitContainer; + + [TitleGroup("关键词系统 (Keyword System)", Alignment = TitleAlignments.Centered)] + + [BoxGroup("关键词系统 (Keyword System)/设置")] + [LabelText("启用关键词高亮")] + [Tooltip("是否在台词中自动识别并高亮关键词")] + [SerializeField] private bool enableKeywordHighlight = true; + + [BoxGroup("关键词系统 (Keyword System)/设置")] + [ShowIf(nameof(enableKeywordHighlight))] + [LabelText("高亮颜色 (Highlight Color)")] + [Tooltip("关键词文本的高亮颜色")] + [SerializeField] private Color keywordHighlightColor = new Color(0.67f, 0.87f, 1f); // #AADDFF + + // 缓存是否已构建的标记,避免每句台词重复构建 + private bool _keywordCacheBuilt = false; + + // 当前台词的 Markup 解析结果,保存到 PostProcessDisplayText 使用 + private MarkupParseResult _currentLineMarkup; + + /// 最后一次说话的角色名称(Yarn 脚本中的 CharacterName) + public static string LastSpeakerName { get; set; } + + /// 当行内 Markup 标记请求播放动画时触发此事件 + public static event Action OnPlayAnimationRequested; + + /// 当行内 Markup 标记请求停止动画时触发此事件 + public static event Action OnStopAnimationRequested; + + // --------------------------------------------------------------- + // 生命周期 + // --------------------------------------------------------------- + + public override YarnTask OnDialogueStartedAsync() + { + // 在对话开始时构建关键词缓存 + if (enableKeywordHighlight && StorySystem.Database != null) + { + KeywordProcessor.BuildCache(StorySystem.Database.keywords); + _keywordCacheBuilt = true; + } + + return base.OnDialogueStartedAsync(); + } + + public override YarnTask OnDialogueCompleteAsync() + { + _keywordCacheBuilt = false; + LastSpeakerName = null; // 对话结束时置空 + return base.OnDialogueCompleteAsync(); + } + + // --------------------------------------------------------------- + // 台词处理 + // --------------------------------------------------------------- + + public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token) + { + if (!string.IsNullOrEmpty(line.CharacterName)) + { + LastSpeakerName = line.CharacterName; // 记录当前说话角色 + } + + // 确保打字机已通过 IAsyncTypewriter 装饰器包裹,防止其在开始打字时抹除关键词富文本标签。 + // 基类 Awake 必然已实例化 Typewriter,在此将其替换为我们的富文本打字机包装器。 + if (Typewriter != null && !(Typewriter is KeywordTypewriterWrapper)) + { + Typewriter = new KeywordTypewriterWrapper(Typewriter, markup => KeywordProcessor.ProcessWithMarkup(markup)); + } + + // 保存当前台词的 Markup,供 PostProcessDisplayText 使用 + _currentLineMarkup = line.TextWithoutCharacterName; + + // 提取并解析 Yarn Markup 行内动作标记 (方案 B) + if (line.TextWithoutCharacterName.Attributes != null) + { + foreach (var attribute in line.TextWithoutCharacterName.Attributes) + { + if (attribute.Name == "anim" || attribute.Name == "play_animation") + { + if (attribute.Properties.TryGetValue(attribute.Name, out var animValue)) + { + string animName = animValue.StringValue; + // 默认指向当前发言说话人,并解析为标准英文 ID + string targetNpc = ResolveStandardCharacterId(line.CharacterName); + + // 同时也支持显式指定其他 NPC,例如 [anim="PushButton" npc="SLS"/] + if (attribute.Properties.TryGetValue("npc", out var npcValue)) + { + targetNpc = ResolveStandardCharacterId(npcValue.StringValue); + } + + OnPlayAnimationRequested?.Invoke(animName, targetNpc); + } + } + else if (attribute.Name == "stop_anim" || attribute.Name == "stop_animation") + { + string targetNpc = ResolveStandardCharacterId(line.CharacterName); + if (attribute.Properties.TryGetValue("npc", out var npcValue)) + { + targetNpc = ResolveStandardCharacterId(npcValue.StringValue); + } + + OnStopAnimationRequested?.Invoke(targetNpc); + } + } + } + + // 在台词展示之前更新立绘 + UpdatePortrait(line); + + // 调用父类处理所有其余逻辑(文本设置、打字机、淡入淡出、等待输入) + // 父类会在 Typewriter.PrepareForContent 之后调用 PostProcessDisplayText() + await base.RunLineAsync(line, token); + } + + /// + /// 由父类 LinePresenter 在 Typewriter.PrepareForContent 之后调用。 + /// 此时文本已完整设置到 TMP 组件上,但打字机尚未开始逐字展示。 + /// 我们在此注入关键词的 link 标签。 + /// + protected override void PostProcessDisplayText() + { + if (!enableKeywordHighlight || !_keywordCacheBuilt) return; + if (lineText == null) return; + + // 使用基于 [kw] Markup 标签的处理,替代正则自动扫描 + lineText.text = KeywordProcessor.ProcessWithMarkup(_currentLineMarkup); + } + + // --------------------------------------------------------------- + // 立绘系统 + // --------------------------------------------------------------- + + private void UpdatePortrait(LocalizedLine line) + { + if (portraitImage == null) return; + + string characterName = line.CharacterName; + + if (string.IsNullOrWhiteSpace(characterName) || StorySystem.Database == null) + { + SetPortraitVisible(false); + return; + } + + CharacterData charData = null; + foreach (var c in StorySystem.Database.characters) + { + if (c != null && (string.Equals(c.nameKey, characterName, StringComparison.OrdinalIgnoreCase) || + c.alias.Contains(characterName))) + { + charData = c; + break; + } + } + + if (charData == null) + { + SetPortraitVisible(false); + return; + } + + YarnTagParser.Parse(line.Metadata, out var kvTags, out _); + kvTags.TryGetValue("mood", out string mood); + + Sprite targetSprite = ResolvePortraitSprite(charData, mood); + + if (targetSprite == null) + { + SetPortraitVisible(false); + return; + } + + portraitImage.sprite = targetSprite; + SetPortraitVisible(true); + } + + private static Sprite ResolvePortraitSprite(CharacterData charData, string mood) + { + if (!string.IsNullOrEmpty(mood) && + charData.expressions != null && + charData.expressions.TryGetValue(mood, out Sprite moodSprite) && + moodSprite != null) + { + return moodSprite; + } + + return charData.defaultPortrait; + } + + private void SetPortraitVisible(bool visible) + { + if (portraitContainer != null) + portraitContainer.SetActive(visible); + else if (portraitImage != null) + portraitImage.enabled = visible; + } + + /// + /// 将 Yarn 传回的本地化说话人名字(如 "引导者")反向解析为系统内部注册的标准英文 ID(如 "Guide")。 + /// + private string ResolveStandardCharacterId(string speakerName) + { + if (string.IsNullOrEmpty(speakerName) || StorySystem.Database == null) + return speakerName; + + foreach (var c in StorySystem.Database.characters) + { + if (c == null) continue; + + // 1. 若已经是标准英文名 (nameKey),直接返回 + if (string.Equals(c.nameKey, speakerName, StringComparison.OrdinalIgnoreCase)) + { + return c.nameKey; + } + + // 2. 若匹配到显示名称 (displayName),则返回对应的标准英文 ID (nameKey) + if (c.alias != null && c.alias.Contains(speakerName)) + { + return c.nameKey; + } + } + + return speakerName; // 未匹配到则保留原样 + } + } + + /// + /// 关键词打字机包装器。 + /// 解决官方打字机在 PrepareForContent 和 RunTypewriter 时, + /// 会强行用 plainText 覆盖 text 组件,导致我们注入的富文本高亮标签被抹除的问题。 + /// + public class KeywordTypewriterWrapper : IAsyncTypewriter + { + private readonly IAsyncTypewriter _inner; + private readonly Func _processMarkupFunc; + + public KeywordTypewriterWrapper(IAsyncTypewriter inner, Func processMarkupFunc) + { + _inner = inner; + _processMarkupFunc = processMarkupFunc; + } + + public TMPro.TMP_Text? TextElement + { + get => _inner.TextElement; + set => _inner.TextElement = value; + } + + public List ActionMarkupHandlers => _inner.ActionMarkupHandlers; + + public void PrepareForContent(MarkupParseResult line) + { + _inner.PrepareForContent(line); + if (TextElement != null) + { + TextElement.text = _processMarkupFunc(line); + } + } + + public async YarnTask RunTypewriter(MarkupParseResult line, System.Threading.CancellationToken cancellationToken) + { + var task = _inner.RunTypewriter(line, cancellationToken); + if (TextElement != null) + { + TextElement.text = _processMarkupFunc(line); + } + await task; + if (TextElement != null) + { + TextElement.text = _processMarkupFunc(line); + } + } + + public void ContentWillDismiss() => _inner.ContentWillDismiss(); + public void ContentDidDismiss() => _inner.ContentDidDismiss(); + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLinePresenter.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLinePresenter.cs.meta new file mode 100644 index 00000000..13a97f49 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedLinePresenter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1762f73b835dbd24f934d49bcb0c3c8f \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs new file mode 100644 index 00000000..1eb25b0c --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs @@ -0,0 +1,128 @@ +using UnityEngine; +using UnityEngine.EventSystems; +using Yarn.Unity; +using Yarn.Markup; +using TMPro; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 高级选项项,继承自官方 OptionItem。 + /// 支持解析选项中的 #desc: 和 #fail: 元数据标签, + /// 通过 OptionTooltipUI 显示选项提示, + /// 以及使用 [kw] Yarn Markup 标签高亮关键词。 + /// + public class AdvancedOptionItem : OptionItem + { + public string TooltipDesc { get; private set; } + public string TooltipFail { get; private set; } + + public TMP_Text GetTextComponent() => text; + + // 追踪当前是否是由鼠标指针触发的选中状态 + // OnPointerEnter 在 OnSelect 之前同步设置此标记,OnDeselect 清除 + private bool _pointerEntered = false; + + public override DialogueOption Option + { + get => base.Option; + set + { + // 调用基类 setter:设置 _option、interactable 和 ApplyStyle + // 注意:基类也会设置 text.text,我们稍后会覆盖它 + base.Option = value; + + // 解析 Tooltip 元数据标签 (#desc: / #fail:) + TooltipDesc = null; + TooltipFail = null; + + if (value.Line.Metadata != null) + { + YarnTagParser.Parse(value.Line.Metadata, out var kvTags, out _); + + if (kvTags.TryGetValue("desc", out string desc)) + { + TooltipDesc = desc.Replace("_", " "); + } + + if (kvTags.TryGetValue("fail", out string fail)) + { + TooltipFail = fail.Replace("_", " "); + } + } + + // 选项文本高亮处理: + // 仅通过 ProcessWithMarkup 处理手动标记的 [kw] 或 [kw id="..."] 标签。 + // 不进行正则自动扫描(只高亮明确被 [kw] 标记的部分,与台词表现一致)。 + if (text != null) + { + string processed = KeywordProcessor.ProcessWithMarkup(value.Line.TextWithoutCharacterName); + + if (disabledStrikeThrough && !value.IsAvailable) + { + processed = $"{processed}"; + } + + text.text = processed; + } + } + } + + public override void OnPointerEnter(PointerEventData eventData) + { + // 在调用 base.OnPointerEnter(其中会同步调用 OnSelect)之前 + // 设置标记,使 OnSelect 能知道这是鼠标触发的 + _pointerEntered = true; + base.OnPointerEnter(eventData); + } + + public override void OnPointerExit(PointerEventData eventData) + { + base.OnPointerExit(eventData); + // 鼠标离开后清除,但不关闭 Tooltip(由 OptionTooltipUI 的文本区域检测控制) + _pointerEntered = false; + } + + public override void OnPointerClick(PointerEventData eventData) + { + // 只有当点击的是鼠标左键时,才允许选择并推进此选项,阻断鼠标右键的选择触发 + if (eventData.button == PointerEventData.InputButton.Left) + { + base.OnPointerClick(eventData); + } + } + + public override void OnSelect(BaseEventData eventData) + { + base.OnSelect(eventData); + + // 将"是否为鼠标触发"传递给 OptionTooltipUI,用于决定定位方式 + OptionTooltipUI.Instance?.OnOptionSelected(this, _pointerEntered); + } + + public override void OnDeselect(BaseEventData eventData) + { + base.OnDeselect(eventData); + _pointerEntered = false; + OptionTooltipUI.Instance?.OnOptionDeselected(this); + } + + protected override void OnEnable() + { + base.OnEnable(); + if (KeywordTooltipUI.Instance != null && text != null) + { + KeywordTooltipUI.Instance.RegisterExternalText(text); + } + } + + protected override void OnDisable() + { + base.OnDisable(); + if (KeywordTooltipUI.Instance != null && text != null) + { + KeywordTooltipUI.Instance.UnregisterExternalText(text); + } + } + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs.meta new file mode 100644 index 00000000..377792c5 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3965a9528049b914baeeff5e76f39162 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionsPresenter.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionsPresenter.cs new file mode 100644 index 00000000..da0f5575 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionsPresenter.cs @@ -0,0 +1,74 @@ +using UnityEngine; +using Yarn.Unity; +using Sirenix.OdinInspector; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 高级选项展现层,继承自官方 OptionsPresenter。 + /// + public class AdvancedOptionsPresenter : OptionsPresenter + { + [TitleGroup("Advanced Settings", Alignment = TitleAlignments.Centered)] + [BoxGroup("Advanced Settings/Prefabs")] + [Required("需要指定 AdvancedOptionItem 预制体")] + [SerializeField] private AdvancedOptionItem advancedOptionViewPrefab; + + public override YarnTask OnDialogueStartedAsync() + { + // 建立关键词缓存,保证 ProcessWithMarkup 能正确查找关键词数据 + // (不依赖 AdvancedLinePresenter 是否已经运行) + if (StorySystem.Database != null) + KeywordProcessor.BuildCache(StorySystem.Database.keywords); + + return base.OnDialogueStartedAsync(); + } + + public override YarnTask RunOptionsAsync(DialogueOption[] dialogueOptions, LineCancellationToken cancellationToken) + { + // 过滤掉不可用且带有 "hide" 或 "#hide" 标签的选项 + var filteredOptions = new System.Collections.Generic.List(); + foreach (var option in dialogueOptions) + { + bool shouldHide = false; + if (!option.IsAvailable && option.Line != null && option.Line.Metadata != null) + { + foreach (var tag in option.Line.Metadata) + { + if (tag.Equals("hide", System.StringComparison.OrdinalIgnoreCase) || + tag.Equals("#hide", System.StringComparison.OrdinalIgnoreCase)) + { + shouldHide = true; + break; + } + } + } + + if (!shouldHide) + { + filteredOptions.Add(option); + } + } + + return base.RunOptionsAsync(filteredOptions.ToArray(), cancellationToken); + } + + protected override OptionItem CreateNewOptionView() + { + var targetTransform = canvasGroup != null ? canvasGroup.transform : this.transform; + + var optionView = Instantiate(advancedOptionViewPrefab, targetTransform, false); + + if (optionView == null) + { + Debug.LogError("Failed to instantiate advancedOptionViewPrefab."); + return null; + } + + optionView.transform.SetAsLastSibling(); + optionView.gameObject.SetActive(false); + + return optionView; + } + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionsPresenter.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionsPresenter.cs.meta new file mode 100644 index 00000000..b4a362f2 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionsPresenter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7e64bee4ae3276841b0949789632d0b4 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs new file mode 100644 index 00000000..3d7e8044 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs @@ -0,0 +1,263 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using UnityEngine; +using Yarn.Markup; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 关键词文本处理器。 + /// 扫描台词文本,将已注册的关键词包裹为 TMP 标签, + /// 使其可被悬停检测和高亮显示。 + /// + public static class KeywordProcessor + { + /// + /// 关键词的 link ID 前缀,用于与其他 link 类型区分。 + /// 例如: + /// + public const string LinkPrefix = "kw:"; + + // 已编译的关键词匹配正则(所有触发词按长度降序排列) + private static Regex _keywordPattern; + + // 触发词 → KeywordData 的映射表(大小写不敏感) + private static Dictionary _triggerLookup; + + // 主关键词 → KeywordData 的映射表(用于通过 link ID 反查) + private static Dictionary _primaryLookup; + + // 用于识别 RichText 标签的正则,避免在标签内部匹配关键词 + private static readonly Regex RichTextTagPattern = + new Regex(@"<[^>]+>", RegexOptions.Compiled); + + /// + /// 从数据库构建关键词缓存。 + /// 应在对话开始时调用一次,或在关键词列表变更后重新调用。 + /// + public static void BuildCache(List keywords) + { + _triggerLookup = new Dictionary(System.StringComparer.OrdinalIgnoreCase); + _primaryLookup = new Dictionary(System.StringComparer.OrdinalIgnoreCase); + + if (keywords == null || keywords.Count == 0) + { + _keywordPattern = null; + return; + } + + var allTriggers = new List(); + + foreach (var kw in keywords) + { + if (kw == null || string.IsNullOrWhiteSpace(kw.keyword)) continue; + + // 注册主关键词到 primaryLookup + if (!_primaryLookup.ContainsKey(kw.keyword)) + _primaryLookup[kw.keyword] = kw; + + // 注册所有触发词(主关键词 + 别名)到 triggerLookup + foreach (var trigger in kw.GetAllTriggerWords()) + { + if (!string.IsNullOrWhiteSpace(trigger) && !_triggerLookup.ContainsKey(trigger)) + { + _triggerLookup[trigger] = kw; + allTriggers.Add(trigger); + } + } + } + + if (allTriggers.Count == 0) + { + _keywordPattern = null; + return; + } + + // 按长度降序排列,确保"灵能者协会"优先于"灵能者"匹配 + allTriggers.Sort((a, b) => b.Length.CompareTo(a.Length)); + + // 构建正则:将所有触发词用 | 连接 + var escapedTriggers = allTriggers.Select(Regex.Escape); + string pattern = string.Join("|", escapedTriggers); + + _keywordPattern = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); + } + + /// + /// 处理台词文本:扫描并将匹配的关键词包裹为带高亮样式的 TMP link 标签。 + /// 会自动跳过已有的 RichText 标签内部,避免破坏现有排版。 + /// + /// 原始台词文本(可能已包含 RichText 标签) + /// 关键词高亮颜色(十六进制,如 "#AADDFF") + /// 处理后的文本 + public static string Process(string rawText, string highlightColor = "#AADDFF") + { + if (_keywordPattern == null || string.IsNullOrEmpty(rawText)) + return rawText; + + // 将文本按"RichText 标签"和"纯文本"交替切分, + // 只对纯文本段进行关键词替换,标签段原样保留。 + var result = new StringBuilder(rawText.Length * 2); + int lastIndex = 0; + + foreach (Match tagMatch in RichTextTagPattern.Matches(rawText)) + { + // 处理标签前的纯文本段 + if (tagMatch.Index > lastIndex) + { + string segment = rawText.Substring(lastIndex, tagMatch.Index - lastIndex); + result.Append(ReplaceKeywordsInSegment(segment, highlightColor)); + } + + // 标签本身原样追加 + result.Append(tagMatch.Value); + lastIndex = tagMatch.Index + tagMatch.Length; + } + + // 处理最后一个标签之后的剩余纯文本 + if (lastIndex < rawText.Length) + { + string remaining = rawText.Substring(lastIndex); + result.Append(ReplaceKeywordsInSegment(remaining, highlightColor)); + } + + return result.ToString(); + } + + /// + /// 基于 Yarn Markup 属性处理台词/选项文本: + /// 将 [kw] 和 [kw id="主关键词"] 标签转换为 TMP link/style 标签。 + /// + /// Yarn 标签语法: + /// [kw]迎雾森林[/kw] —— 关键词 = 标签内的文本 + /// [kw id="迎雾森林"]林地[/kw] —— 关键词 = “迎雾森林”,显示文本 = “林地” + /// + /// 来自 Yarn 的 MarkupParseResult(LocalizedLine.TextWithoutCharacterName 等) + /// 包含 TMP 忌工标签的处理后字符串 + public static string ProcessWithMarkup(MarkupParseResult markup) + { + string plainText = markup.Text; + if (string.IsNullOrEmpty(plainText)) return plainText; + + // 收集所有 "kw" 属性 + var kwAttributes = new List(); + foreach (var attr in markup.Attributes) + { + if (attr.Name == "kw") + kwAttributes.Add(attr); + } + + // 没有关键词标签,直接返回原始文本 + if (kwAttributes.Count == 0) return plainText; + + // 按位置降序排列,从字符串末尾开始插入,避免早期插入造成索引偏移 + kwAttributes.Sort((a, b) => b.Position.CompareTo(a.Position)); + + var sb = new StringBuilder(plainText); + + foreach (var attr in kwAttributes) + { + // 确定关键词:优先使用 id 属性,否则取标签覆盖的文本 + string keyword; + if (attr.Properties.TryGetValue("id", out var idProp)) + { + keyword = idProp.StringValue; + } + else + { + keyword = plainText.Substring(attr.Position, attr.Length); + } + + // 在数据库中查找该关键词,找不到则跳过(不插入任何标签) + if (FindByPrimaryKeyword(keyword) == null) continue; + + // 先插入闭合标签(索引较大),再插入开放标签 + sb.Insert(attr.Position + attr.Length, ""); + sb.Insert(attr.Position, $""); + } + + return sb.ToString(); + } + + /// + /// 处理关键词的解释文本(用于嵌套 Tooltip)。 + /// 与 Process 相同,但会排除自身关键词,避免自引用循环。 + /// + public static string ProcessDescription(string description, string excludeKeyword, + string highlightColor = "#AADDFF") + { + if (_keywordPattern == null || string.IsNullOrEmpty(description)) + return description; + + var result = new StringBuilder(description.Length * 2); + int lastIndex = 0; + + foreach (Match tagMatch in RichTextTagPattern.Matches(description)) + { + if (tagMatch.Index > lastIndex) + { + string segment = description.Substring(lastIndex, tagMatch.Index - lastIndex); + result.Append(ReplaceKeywordsInSegment(segment, highlightColor, excludeKeyword)); + } + result.Append(tagMatch.Value); + lastIndex = tagMatch.Index + tagMatch.Length; + } + + if (lastIndex < description.Length) + { + string remaining = description.Substring(lastIndex); + result.Append(ReplaceKeywordsInSegment(remaining, highlightColor, excludeKeyword)); + } + + return result.ToString(); + } + + /// + /// 通过 link ID(去掉 "kw:" 前缀后的主关键词)反查对应的 KeywordData。 + /// + public static KeywordData FindByPrimaryKeyword(string primaryKeyword) + { + if (_primaryLookup == null || string.IsNullOrEmpty(primaryKeyword)) + return null; + + _primaryLookup.TryGetValue(primaryKeyword, out var result); + return result; + } + + /// + /// 从 TMP link ID 字符串中提取主关键词。 + /// 例如输入 "kw:灵能者",返回 "灵能者"。 + /// 如果不是关键词类型的 link,返回 null。 + /// + public static string ExtractKeywordFromLinkId(string linkId) + { + if (string.IsNullOrEmpty(linkId) || !linkId.StartsWith(LinkPrefix)) + return null; + + return linkId.Substring(LinkPrefix.Length); + } + + // ------------------------------------------------------------------- + + private static string ReplaceKeywordsInSegment(string segment, string color, + string excludeKeyword = null) + { + return _keywordPattern.Replace(segment, match => + { + if (!_triggerLookup.TryGetValue(match.Value, out var kwData)) + return match.Value; + + // 排除自身关键词(用于嵌套 Tooltip 防止自引用) + if (excludeKeyword != null && + string.Equals(kwData.keyword, excludeKeyword, System.StringComparison.OrdinalIgnoreCase)) + return match.Value; + + // 包裹为 TMP link 标签 + // link ID 格式: "kw:主关键词" + return $"{match.Value}"; + }); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs.meta new file mode 100644 index 00000000..7216ea24 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 045da86a92e1064419ec0949cf6a3c51 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs new file mode 100644 index 00000000..b2ead9cc --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs @@ -0,0 +1,337 @@ +using System.Collections; +using System.Collections.Generic; +using Sirenix.OdinInspector; +using TMPro; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 关键词浮动窗口管理器。 + /// 检测鼠标在 TMP 文本上悬停的 link 标签,弹出关键词解释窗口。 + /// 支持嵌套窗口、右键固定、点击外部关闭,以及左键关闭时阻断台词推进。 + /// + /// 面板的实际内容显示、定位和固定状态由 组件管理, + /// 本类仅负责悬停检测、生命周期编排和输入分发。 + /// + public class KeywordTooltipUI : MonoBehaviour + { + // --------------------------------------------------------------- + // 静态属性:供 LineAdvancer 查询是否需要阻断本帧输入 + // --------------------------------------------------------------- + + /// + /// 当任意 Tooltip 窗口处于打开状态时为 true。 + /// LineAdvancer 应在处理台词推进前检查此值。 + /// + public static bool IsBlockingDialogueInput { get; private set; } + + public static KeywordTooltipUI Instance { get; private set; } + + // --------------------------------------------------------------- + // Inspector 配置 + // --------------------------------------------------------------- + + [TitleGroup("核心引用 (Core References)", Alignment = TitleAlignments.Centered)] + + [BoxGroup("核心引用 (Core References)/UI")] + [Required("需要指定 Tooltip 面板的 Prefab(必须挂载 TooltipPanel 组件)")] + [SerializeField] private GameObject tooltipPanelPrefab; + + [BoxGroup("核心引用 (Core References)/UI")] + [Required("Tooltip 生成的父级容器(RectTransform)")] + [SerializeField] private RectTransform tooltipContainer; + + [BoxGroup("核心引用 (Core References)/UI")] + [Tooltip("主台词文本组件,用于检测鼠标悬停的关键词链接")] + [SerializeField] private TMP_Text mainLineText; + + [BoxGroup("核心引用 (Core References)/UI")] + [Tooltip("渲染 Canvas 的摄像机。Screen Space Overlay 模式下留空")] + [SerializeField] private Camera uiCamera; + + [TitleGroup("行为设置 (Behavior Settings)", Alignment = TitleAlignments.Centered)] + + [BoxGroup("行为设置 (Behavior Settings)/定位")] + [Tooltip("Tooltip 左下角相对于鼠标的屏幕像素偏移量")] + [SerializeField] private Vector2 tooltipOffset = new Vector2(12f, 12f); + + // --------------------------------------------------------------- + // 内部状态 + // --------------------------------------------------------------- + + // 所有当前打开的 Tooltip 面板(包含固定的和悬停的) + private readonly List _openPanels = new List(); + + // 当前唯一的悬停 Tooltip(未固定,跟随鼠标) + private TooltipPanel _hoverPanel; + + // 上一帧检测到的悬停关键词 + private string _lastHoveredKeyword; + + // 供外部(如 OptionTooltipUI)注册的额外检测文本 + private readonly List _externalTexts = new List(); + + /// + /// 当前是否有未固定的悬停面板 + /// + public bool HasHoverPanel => _hoverPanel != null; + + // --------------------------------------------------------------- + // Unity 生命周期 + // --------------------------------------------------------------- + + private void Awake() + { + Instance = this; + } + + private void OnDisable() + { + CloseAllTooltips(); + } + + private void Update() + { + HandleHoverDetection(); + HandleClickInput(); + } + + // --------------------------------------------------------------- + // 悬停检测 + // --------------------------------------------------------------- + + private void HandleHoverDetection() + { + Vector2 mousePos = Mouse.current.position.ReadValue(); + + // 检测当前鼠标命中的关键词链接 + string hoveredKeyword = DetectHoveredKeyword(mousePos); + + // 如果鼠标没命中链接,但在当前 Hover 面板内,保持悬停状态不变 + bool mouseInsideHoverPanel = _hoverPanel != null && + _hoverPanel.ContainsScreenPoint(mousePos, uiCamera); + + if (mouseInsideHoverPanel && hoveredKeyword == null) + { + // 鼠标从链接移到了 Tooltip 面板 → 保持显示,不移动位置 + return; + } + + // 悬停目标变化 → 刷新 Hover Tooltip + if (hoveredKeyword != _lastHoveredKeyword) + { + _lastHoveredKeyword = hoveredKeyword; + CloseHoverTooltip(); + + if (!string.IsNullOrEmpty(hoveredKeyword)) + { + // 如果该关键词已经有固定窗口存在 → 不创建新的 Hover + if (!HasPinnedPanelForKeyword(hoveredKeyword)) + { + var kwData = KeywordProcessor.FindByPrimaryKeyword(hoveredKeyword); + if (kwData != null) + _hoverPanel = SpawnPanel(kwData, mousePos, pinned: false); + } + } + } + + // 跟随鼠标更新位置(仅对 Hover 面板) + if (_hoverPanel != null && !_hoverPanel.IsPinned) + _hoverPanel.PositionAtScreenPoint(mousePos, tooltipOffset); + } + + private string DetectHoveredKeyword(Vector2 mousePos) + { + // 先检测主台词文本 + string kw = DetectLinkAt(mainLineText, mousePos); + if (kw != null) return kw; + + // 再检测所有已打开面板内的描述文本(支持嵌套) + foreach (var panel in _openPanels) + { + kw = DetectLinkAt(panel.DescriptionText, mousePos); + if (kw != null) return kw; + } + + // 最后检测外部注册的文本(如选项文本) + foreach (var extText in _externalTexts) + { + kw = DetectLinkAt(extText, mousePos); + if (kw != null) return kw; + } + + return null; + } + + // --------------------------------------------------------------- + // 点击输入处理 + // --------------------------------------------------------------- + + private void HandleClickInput() + { + bool leftClick = Mouse.current.leftButton.wasPressedThisFrame; + bool rightClick = Mouse.current.rightButton.wasPressedThisFrame; + + if (!leftClick && !rightClick) return; + if (_openPanels.Count == 0) return; + + Vector2 mousePos = Mouse.current.position.ReadValue(); + + // 检测是否右键点击在关键词链接上 → 固定 Hover 面板 + if (rightClick) + { + string clickedKeyword = DetectHoveredKeyword(mousePos); + if (!string.IsNullOrEmpty(clickedKeyword) && + _hoverPanel != null && + _hoverPanel.Keyword == clickedKeyword) + { + PinHoverPanel(); + return; + } + } + + // 检测是否点击在任意 Tooltip 面板内部 → 如果是则不处理 + if (IsMouseInsideAnyPanel(mousePos)) return; + + // 点击在所有 Tooltip 外部 → 关闭所有 Tooltip + CloseAllTooltips(); + + // 左键关闭时,本帧阻断台词推进(下一帧自动解除) + if (leftClick) + { + IsBlockingDialogueInput = true; + StartCoroutine(UnblockNextFrame()); + } + } + + // --------------------------------------------------------------- + // 面板生命周期 + // --------------------------------------------------------------- + + private TooltipPanel SpawnPanel(KeywordData data, Vector2 screenPos, bool pinned) + { + if (tooltipPanelPrefab == null || tooltipContainer == null) return null; + + var panelGO = Instantiate(tooltipPanelPrefab, tooltipContainer); + var panel = panelGO.GetComponent(); + + if (panel == null) + { + Debug.LogError( + $"[KeywordTooltipUI] Tooltip Prefab 上缺少 TooltipPanel 组件!" + + $"请确保 Prefab '{tooltipPanelPrefab.name}' 挂载了 TooltipPanel 脚本。", + tooltipPanelPrefab); + Destroy(panelGO); + return null; + } + + panel.Initialize(data, pinned); + panel.PositionAtScreenPoint(screenPos, tooltipOffset); + + _openPanels.Add(panel); + IsBlockingDialogueInput = true; + + return panel; + } + + private void PinHoverPanel() + { + if (_hoverPanel == null) return; + + _hoverPanel.Pin(); + + _hoverPanel = null; + _lastHoveredKeyword = null; + } + + private void CloseHoverTooltip() + { + if (_hoverPanel == null) return; + + _openPanels.Remove(_hoverPanel); + if (_hoverPanel.gameObject != null) + Destroy(_hoverPanel.gameObject); + + _hoverPanel = null; + + if (_openPanels.Count == 0) + IsBlockingDialogueInput = false; + } + + private void CloseAllTooltips() + { + foreach (var panel in _openPanels) + { + if (panel != null && panel.gameObject != null) + Destroy(panel.gameObject); + } + _openPanels.Clear(); + _hoverPanel = null; + _lastHoveredKeyword = null; + IsBlockingDialogueInput = false; + } + + // --------------------------------------------------------------- + // 查询方法 + // --------------------------------------------------------------- + + /// + /// 检查指定关键词是否已有固定的面板存在。 + /// 用于避免为同一个关键词生成重复的 Hover 面板。 + /// + private bool HasPinnedPanelForKeyword(string keyword) + { + foreach (var panel in _openPanels) + { + if (panel.IsPinned && panel.Keyword == keyword) + return true; + } + return false; + } + + // --------------------------------------------------------------- + // 工具方法 + // --------------------------------------------------------------- + + private string DetectLinkAt(TMP_Text tmpText, Vector2 screenPos) + { + if (tmpText == null) return null; + + int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmpText, screenPos, uiCamera); + if (linkIndex < 0) return null; + + string linkId = tmpText.textInfo.linkInfo[linkIndex].GetLinkID(); + return KeywordProcessor.ExtractKeywordFromLinkId(linkId); + } + + private bool IsMouseInsideAnyPanel(Vector2 screenPos) + { + foreach (var panel in _openPanels) + { + if (panel != null && panel.ContainsScreenPoint(screenPos, uiCamera)) + return true; + } + return false; + } + + private IEnumerator UnblockNextFrame() + { + yield return null; + IsBlockingDialogueInput = false; + } + + public void RegisterExternalText(TMP_Text text) + { + if (text != null && !_externalTexts.Contains(text)) + _externalTexts.Add(text); + } + + public void UnregisterExternalText(TMP_Text text) + { + if (text != null) + _externalTexts.Remove(text); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs.meta new file mode 100644 index 00000000..8bcc1b7c --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 78e77e9842502d948928cf5cb7c814d7 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs new file mode 100644 index 00000000..8135105e --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs @@ -0,0 +1,330 @@ +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.InputSystem; +using Sirenix.OdinInspector; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 选项悬停提示面板管理器。 + /// - 鼠标模式:仅在鼠标悬停于选项"文本区域"(非空白行区域)时显示,跟随鼠标移动。 + /// - 键盘模式:在选项文本的右上角处固定显示。 + /// - 支持右键固定和点击外部关闭。 + /// + public class OptionTooltipUI : MonoBehaviour + { + public static OptionTooltipUI Instance { get; private set; } + + [TitleGroup("核心引用 (Core References)", Alignment = TitleAlignments.Centered)] + + [BoxGroup("核心引用 (Core References)/UI")] + [Required("需要指定 Tooltip 面板的 Prefab(必须挂载 TooltipPanel 组件)")] + [SerializeField] private GameObject tooltipPanelPrefab; + + [BoxGroup("核心引用 (Core References)/UI")] + [Required("Tooltip 生成的父级容器(RectTransform)")] + [SerializeField] private RectTransform tooltipContainer; + + [BoxGroup("核心引用 (Core References)/UI")] + [Tooltip("渲染 Canvas 的摄像机。Screen Space Overlay 模式下留空")] + [SerializeField] private Camera uiCamera; + + [TitleGroup("行为设置 (Behavior Settings)", Alignment = TitleAlignments.Centered)] + [BoxGroup("行为设置 (Behavior Settings)/定位")] + [Tooltip("Tooltip 左下角相对于鼠标(或键盘时文本右上角)的像素偏移量")] + [SerializeField] private Vector2 tooltipOffset = new Vector2(12f, 12f); + + [BoxGroup("行为设置 (Behavior Settings)/定位")] + [Tooltip("鼠标检测的边缘容差像素数。\n较大值可避免中文全角标点符号边缘闪烁,较小值则更精确地限制在文字内。")] + [Range(0f, 20f)] + [SerializeField] private float textBoundsTolerance = 6f; + + private TooltipPanel _hoverPanel; + private AdvancedOptionItem _hoverItem; + + // 当前悬停是否由鼠标触发(false = 键盘触发) + private bool _isMouseSelection; + + // 已固定的选项 Tooltip + private readonly List _pinnedPanels = new List(); + + private void Awake() + { + Instance = this; + } + + private void OnDisable() + { + CloseHoverPanel(); + CloseAllPinnedPanels(); + } + + private void Update() + { + HandleHoverPanelVisibility(); + HandleClickInput(); + } + + // --------------------------------------------------------------- + // 公开接口(由 AdvancedOptionItem 调用) + // --------------------------------------------------------------- + + public void OnOptionSelected(AdvancedOptionItem item, bool isMouseTriggered) + { + _hoverItem = item; + _isMouseSelection = isMouseTriggered; + + string textToShow = item.Option.IsAvailable ? item.TooltipDesc : item.TooltipFail; + if (string.IsNullOrWhiteSpace(textToShow)) + { + CloseHoverPanel(); + return; + } + + if (HasPinnedPanelForOption(textToShow)) + { + CloseHoverPanel(); + return; + } + + CloseHoverPanel(); + _hoverPanel = SpawnPanel(textToShow); + + if (_isMouseSelection) + { + // 鼠标模式:初始位置对齐鼠标,后续每帧跟随 + Vector2 mousePos = Mouse.current.position.ReadValue(); + _hoverPanel.PositionAtScreenPoint(mousePos, tooltipOffset); + } + else + { + // 键盘模式:定位在文本右上角处 + PositionPanelAtTextTopRight(_hoverPanel, item.GetTextComponent()); + } + } + + public void OnOptionDeselected(AdvancedOptionItem item) + { + if (_hoverItem == item) + CloseHoverPanel(); + } + + // --------------------------------------------------------------- + // 每帧更新(悬停面板可见性与定位) + // --------------------------------------------------------------- + + private void HandleHoverPanelVisibility() + { + if (_hoverPanel == null) return; + + // 当选项文本中出现了关键词且玩家正在选中关键词时,隐去未固定的选项 Tooltip + bool isHoveringKeyword = KeywordTooltipUI.Instance != null && KeywordTooltipUI.Instance.HasHoverPanel; + if (isHoveringKeyword) + { + _hoverPanel.gameObject.SetActive(false); + return; + } + + if (_isMouseSelection) + { + Vector2 mousePos = Mouse.current.position.ReadValue(); + + // 使用 textBounds 检测文本渲染边界,避免全角标点符号字形间隙造成闪烁 + var textComp = _hoverItem?.GetTextComponent(); + bool mouseOverText = IsMouseOverTextArea(textComp, mousePos); + + if (mouseOverText) + { + _hoverPanel.gameObject.SetActive(true); + // 每帧跟随鼠标 + _hoverPanel.PositionAtScreenPoint(mousePos, tooltipOffset); + } + else + { + _hoverPanel.gameObject.SetActive(false); + } + } + else + { + // 键盘模式:始终显示,位置固定在文本右上角(无需每帧更新) + _hoverPanel.gameObject.SetActive(true); + } + } + + // --------------------------------------------------------------- + // 点击输入处理 + // --------------------------------------------------------------- + + private void HandleClickInput() + { + bool leftClick = Mouse.current.leftButton.wasPressedThisFrame; + bool rightClick = Mouse.current.rightButton.wasPressedThisFrame; + + if (!leftClick && !rightClick) return; + + Vector2 mousePos = Mouse.current.position.ReadValue(); + + // 右键固定:当 hover panel 可见时,右键单击在选项区域内将其固定 + if (rightClick && _hoverPanel != null && _hoverPanel.gameObject.activeSelf) + { + bool isHoveringKeyword = KeywordTooltipUI.Instance != null && KeywordTooltipUI.Instance.HasHoverPanel; + if (!isHoveringKeyword) + { + // 使用与悬停检测相同的 textBounds 方式 + var textComp = _hoverItem?.GetTextComponent(); + bool clickOverText = IsMouseOverTextArea(textComp, mousePos); + bool clickOverPanel = _hoverPanel.ContainsScreenPoint(mousePos, uiCamera); + + if (clickOverText || clickOverPanel) + { + PinHoverPanel(); + return; + } + } + } + + // 点击外部关闭所有已固定的选项 Tooltip + if (_pinnedPanels.Count > 0) + { + bool clickedInside = false; + + foreach (var panel in _pinnedPanels) + { + if (panel.ContainsScreenPoint(mousePos, uiCamera)) + { + clickedInside = true; + break; + } + } + + if (!clickedInside && _hoverPanel != null && _hoverPanel.ContainsScreenPoint(mousePos, uiCamera)) + clickedInside = true; + + if (!clickedInside) + CloseAllPinnedPanels(); + } + } + + // --------------------------------------------------------------- + // 面板生命周期 + // --------------------------------------------------------------- + + private void PinHoverPanel() + { + if (_hoverPanel == null) return; + + _hoverPanel.Pin(); + _pinnedPanels.Add(_hoverPanel); + _hoverPanel = null; + } + + private TooltipPanel SpawnPanel(string description) + { + if (tooltipPanelPrefab == null || tooltipContainer == null) return null; + + var panelGO = Instantiate(tooltipPanelPrefab, tooltipContainer); + var panel = panelGO.GetComponent(); + + // 创建临时 KeywordData,内容为选项说明(无标题) + var data = ScriptableObject.CreateInstance(); + data.keyword = string.Empty; + // 选项说明中也支持嵌套关键词:由 Initialize 内的 ProcessDescription 自动处理 + data.description = description; + + panel.Initialize(data, false); + + Destroy(data); + return panel; + } + + // --------------------------------------------------------------- + // 定位工具 + // --------------------------------------------------------------- + + /// + /// 将面板定位在 TMP 文本组件的右上角处(用于键盘模式)。 + /// + private void PositionPanelAtTextTopRight(TooltipPanel panel, TMP_Text textComp) + { + if (panel == null || textComp == null) return; + + Vector3[] corners = new Vector3[4]; + textComp.rectTransform.GetWorldCorners(corners); + // corners 顺序:0=BL, 1=TL, 2=TR, 3=BR(屏幕坐标,Overlay模式) + // 对于非 Overlay 模式,使用 WorldToScreenPoint 转换 + Vector2 screenPos = uiCamera != null + ? RectTransformUtility.WorldToScreenPoint(uiCamera, corners[2]) + : new Vector2(corners[2].x, corners[2].y); + + panel.PositionAtScreenPoint(screenPos, tooltipOffset); + } + + // --------------------------------------------------------------- + // 文本区域检测工具 + // --------------------------------------------------------------- + + /// + /// 检测鼠标屏幕坐标是否处于 TMP 文本的实际渲染边界矩形内。 + /// 使用 textBounds(字形渲染包围盒)而非 FindIntersectingCharacter, + /// 可避免中文全角标点符号字形内空白区域导致的闪烁问题。 + /// + private bool IsMouseOverTextArea(TMP_Text textComp, Vector2 screenMousePos) + { + if (textComp == null) return false; + + // 确保 TMPro 网格在当下完成同步刷新,以获得 100% 准确的渲染包围盒,彻底阻断首帧零包围盒渲染计算闪烁 + textComp.ForceMeshUpdate(); + + // 将屏幕坐标转换为 TMP RectTransform 的局部坐标 + if (!RectTransformUtility.ScreenPointToLocalPointInRectangle( + textComp.rectTransform, screenMousePos, uiCamera, out Vector2 localPoint)) + return false; + + // textBounds 是 TMP 实际渲染内容的包围盒(局部坐标), + // 比 RectTransform 本身更精确,且不受字符个体差异影响 + Bounds bounds = textComp.textBounds; + + // 加入可配置容差,避免全角标点字形边缘闪烁 + return localPoint.x >= bounds.min.x - textBoundsTolerance + && localPoint.x <= bounds.max.x + textBoundsTolerance + && localPoint.y >= bounds.min.y - textBoundsTolerance + && localPoint.y <= bounds.max.y + textBoundsTolerance; + } + + // --------------------------------------------------------------- + // 查询工具 + // --------------------------------------------------------------- + + private bool HasPinnedPanelForOption(string description) + { + // 简单比较原始描述文本(未处理),避免二次处理比较问题 + foreach (var panel in _pinnedPanels) + { + if (panel != null && panel.DescriptionText != null && + panel.DescriptionText.text.Contains(description.Substring(0, Mathf.Min(description.Length, 10)))) + { + return true; + } + } + return false; + } + + private void CloseHoverPanel() + { + if (_hoverPanel != null && _hoverPanel.gameObject != null) + Destroy(_hoverPanel.gameObject); + _hoverPanel = null; + } + + private void CloseAllPinnedPanels() + { + foreach (var panel in _pinnedPanels) + { + if (panel != null && panel.gameObject != null) + Destroy(panel.gameObject); + } + _pinnedPanels.Clear(); + } + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs.meta new file mode 100644 index 00000000..08956e03 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dab16e5156d7d044b83244748ea29ab7 \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset b/Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset new file mode 100644 index 00000000..f2f80dca --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4d05a01a65ed2dfa3a643029cfa273b91c89be722127488ef0e1f30a0801725 +size 1011 diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset.meta new file mode 100644 index 00000000..2e7451f0 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2aa296e25917c8b468ed9c19f3c90b38 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs b/Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs new file mode 100644 index 00000000..839e1eb9 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs @@ -0,0 +1,269 @@ +using Sirenix.OdinInspector; +using TMPro; +using UnityEngine; +using UnityEngine.Serialization; +using UnityEngine.UI; + +namespace SLSUtilities.Narrative.UI +{ + /// + /// 关键词浮动面板组件。 + /// 挂载在 Tooltip Prefab 根节点上,负责管理单个面板的内容显示、 + /// 固定状态及屏幕定位。 + /// + public class TooltipPanel : MonoBehaviour + { + // --------------------------------------------------------------- + // Inspector 配置(Prefab 内拖拽赋值) + // --------------------------------------------------------------- + + [TitleGroup("面板引用 (Panel References)", Alignment = TitleAlignments.Centered)] + + [BoxGroup("面板引用 (Panel References)/标题行")] + [LabelText("标题栏容器")] + [SerializeField] private RectTransform titleBarContainer; + + [BoxGroup("面板引用 (Panel References)/标题行")] + [LabelText("标题文本")] + [SerializeField] private TMP_Text titleText; + + [BoxGroup("面板引用 (Panel References)/标题行")] + [LabelText("关键词图标")] + [SerializeField] private Image iconImage; + + [FormerlySerializedAs("pinIndicator")] + [BoxGroup("面板引用 (Panel References)/标题行")] + [LabelText("固定指示器")] + [SerializeField] private GameObject titlePin; + + [BoxGroup("面板引用 (Panel References)/描述行")] + [LabelText("描述栏容器")] + [SerializeField] private RectTransform descriptionContainer; + + [BoxGroup("面板引用 (Panel References)/描述行")] + [LabelText("描述文本")] + [SerializeField] private TMP_Text descriptionText; + + [BoxGroup("面板引用 (Panel References)/描述行")] + [LabelText("固定指示器")] + [SerializeField] private GameObject descriptionPin; + + // --------------------------------------------------------------- + // 公开属性 + // --------------------------------------------------------------- + + /// + /// 此面板对应的主关键词。 + /// + public string Keyword { get; private set; } + + /// + /// 该面板是否已被固定(右键固定后不再跟随鼠标,且不会因移开鼠标而关闭)。 + /// + public bool IsPinned { get; private set; } + + /// + /// 面板的 RectTransform 引用,供外部定位和碰撞检测。 + /// + public RectTransform Rect { get; private set; } + + /// + /// 面板内的描述文本组件引用,供外部检测嵌套链接。 + /// + public TMP_Text DescriptionText => descriptionText; + + // --------------------------------------------------------------- + // 生命周期 + // --------------------------------------------------------------- + + private void Awake() + { + Rect = GetComponent(); + } + + // --------------------------------------------------------------- + // 初始化 + // --------------------------------------------------------------- + + /// + /// 初始化面板内容。由 KeywordTooltipUI 在实例化后调用。 + /// + /// 关键词数据 + /// 是否初始即为固定状态 + public void Initialize(KeywordData data, bool pinned) + { + Keyword = data.keyword; + IsPinned = pinned; + + // 1. 标题与标题栏显隐控制 + bool hasTitle = !string.IsNullOrEmpty(data.keyword); + if (titleText != null) + titleText.text = data.keyword; + + if (titleBarContainer != null) + titleBarContainer.gameObject.SetActive(hasTitle); + + // 2. 描述(经过关键词处理,支持嵌套链接,排除自身防止自引用) + if (descriptionText != null) + { + string processed = KeywordProcessor.ProcessDescription( + data.description, data.keyword); + + // 性能与排版双重防御: + // 获取或动态添加 LayoutElement 元素。在 Horizontal Layout Group 中, + // 如果不使用 LayoutElement.preferredWidth 限制,子节点的 TMP_Text 组件 + // 会被 Layout 强制拉伸压缩至其 Minimum Width(即单个中文字符宽度,产生“过窄”Bug)。 + var textLayout = descriptionText.GetComponent(); + if (textLayout == null) + textLayout = descriptionText.gameObject.AddComponent(); + + var containerLayout = descriptionContainer != null ? descriptionContainer.GetComponent() : null; + if (descriptionContainer != null && containerLayout == null) + containerLayout = descriptionContainer.gameObject.AddComponent(); + + // 暂时关闭自动换行以计算其“自然无换行的 preferredWidth” + descriptionText.textWrappingMode = TextWrappingModes.NoWrap; + descriptionText.text = processed; + + // 强制更新 TMPro 字形数据以获取精确的 preferredWidth + descriptionText.ForceMeshUpdate(); + float preferredWidth = descriptionText.preferredWidth; + + // 左右各缩减 10 像素,所以 padding 占用共 20 像素 + float paddingWidth = 20f; + + if (preferredWidth > 980f) + { + // 超过 980 像素,限制文本 preferredWidth 为 980,并启用自动折行 + textLayout.preferredWidth = 980f; + descriptionText.textWrappingMode = TextWrappingModes.Normal; + + if (containerLayout != null) + containerLayout.preferredWidth = 1000f; // 容器宽度 = 980px + 20px padding + } + else + { + // 在 980 像素内,紧贴真实内容宽度展示,不进行折行 + textLayout.preferredWidth = preferredWidth; + descriptionText.textWrappingMode = TextWrappingModes.NoWrap; + + if (containerLayout != null) + containerLayout.preferredWidth = preferredWidth + paddingWidth; + } + } + + // 3. 固定指示器与固定状态 + UpdatePinIndicator(); + + // 4. 性能优化:只在内容加载、文本大小发生改变时强制刷新一次 UI 布局,防止跟随鼠标时每帧高频刷新 + LayoutRebuilder.ForceRebuildLayoutImmediate(Rect); + } + + // --------------------------------------------------------------- + // 固定 / 取消固定 + // --------------------------------------------------------------- + + /// + /// 将面板设为固定状态。固定后不再跟随鼠标,且不会因移开鼠标而自动关闭。 + /// + public void Pin() + { + IsPinned = true; + UpdatePinIndicator(); + } + + /// + /// 取消固定状态。 + /// + public void Unpin() + { + IsPinned = false; + UpdatePinIndicator(); + } + + // --------------------------------------------------------------- + // 屏幕定位 + // --------------------------------------------------------------- + + /// + /// 将面板定位到指定的屏幕坐标。 + /// 默认情况下,面板左下角与鼠标对齐; + /// 在靠近屏幕边缘时,会自动调整到合适位置。 + /// + /// 鼠标屏幕坐标 + /// 基础偏移量 + public void PositionAtScreenPoint(Vector2 screenPos, Vector2 offset) + { + if (Rect == null) return; + + float panelWidth = Rect.rect.width; + float panelHeight = Rect.rect.height; + + // 视觉边缘细节:引入 16 像素的安全屏幕边缘 padding,防止边缘阴影或外发光被物理截边 + float safeMargin = 16f; + + // 基础定位:面板左下角对齐鼠标位置(鼠标在面板的左下角) + // screenPos 即面板的左下角坐标,再加一个小偏移 + float posX = screenPos.x + offset.x; + float posY = screenPos.y + offset.y; + + // 边缘自适应 ─ 右边界 + if (panelWidth > 0 && posX + panelWidth > Screen.width - safeMargin) + { + // 面板会超出右侧 → 改为右下角对齐鼠标(面板在鼠标左侧) + posX = screenPos.x - panelWidth - Mathf.Abs(offset.x); + } + + // 边缘自适应 ─ 左边界 + if (posX < safeMargin) + { + posX = safeMargin; + } + + // 边缘自适应 ─ 上边界 + if (posY + panelHeight > Screen.height - safeMargin) + { + // 面板会超出上方 → 向下调整 + posY = Screen.height - panelHeight - safeMargin; + } + + // 边缘自适应 ─ 下边界 + if (posY < safeMargin) + { + posY = safeMargin; + } + + // 设置 Pivot 为左下角 (0, 0) 以匹配我们的定位逻辑 + Rect.pivot = new Vector2(0f, 0f); + Rect.position = new Vector2(posX, posY); + } + + // --------------------------------------------------------------- + // 碰撞检测 + // --------------------------------------------------------------- + + /// + /// 检测指定的屏幕坐标是否在面板区域内。 + /// + public bool ContainsScreenPoint(Vector2 screenPos, Camera uiCamera) + { + return Rect != null && + RectTransformUtility.RectangleContainsScreenPoint(Rect, screenPos, uiCamera); + } + + // --------------------------------------------------------------- + // 内部方法 + // --------------------------------------------------------------- + + private void UpdatePinIndicator() + { + bool hasTitle = !string.IsNullOrEmpty(Keyword); + + if (titlePin != null) + titlePin.SetActive(IsPinned && hasTitle); + + if (descriptionPin != null) + descriptionPin.SetActive(IsPinned && !hasTitle); + } + } +} diff --git a/Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs.meta b/Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs.meta new file mode 100644 index 00000000..5e4fab25 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b9480658e91a4d548a18e05958b5605f \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/SLSUtilities.asmdef b/Assets/Scripts/SLSUtilities/SLSUtilities.asmdef new file mode 100644 index 00000000..5069b9f7 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/SLSUtilities.asmdef @@ -0,0 +1,23 @@ +{ + "name": "SLSUtilities", + "rootNamespace": "", + "references": [ + "GUID:174bc9f391f1b7f4292b3824c5019a21", + "GUID:560b04d1a97f54a4e82edc0cbbb69285", + "GUID:34aa492b82754644eac2f903cd496268", + "GUID:6055be8ebefd69e48b49212b09b47b2f", + "GUID:75469ad4d38634e559750d17036d5f7c", + "GUID:cfcd2ce455f8d1944942cdd919ecaa60", + "GUID:8017400dc3a8d3c4e8a805361276efd0", + "GUID:bf41a3c927b459f40a6588443b81113c" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/SLSUtilities/SLSUtilities.asmdef.meta b/Assets/Scripts/SLSUtilities/SLSUtilities.asmdef.meta new file mode 100644 index 00000000..94097694 --- /dev/null +++ b/Assets/Scripts/SLSUtilities/SLSUtilities.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 43bb4e992d9b32b4bbb25402b41e80a0 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SLSUtilities/WwiseAssistance/AudioManager.cs b/Assets/Scripts/SLSUtilities/WwiseAssistance/AudioManager.cs index 58cdc420..4c25f376 100644 --- a/Assets/Scripts/SLSUtilities/WwiseAssistance/AudioManager.cs +++ b/Assets/Scripts/SLSUtilities/WwiseAssistance/AudioManager.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using UnityEngine; using AK.Wwise; -using Ichni; using Lean.Pool; using Sirenix.OdinInspector; using SLSUtilities.General; @@ -15,10 +14,7 @@ namespace SLSUtilities.WwiseAssistance public GameObject audioPoint; public List soundBanks; - [Title("Subsystems")] - public SongPlayer backgroundMusicManager; - - private static Dictionary TrackedPlayingIDs = new Dictionary(); + private static Dictionary _trackedPlayingIDs = new Dictionary(); protected override void Awake() { @@ -88,7 +84,7 @@ namespace SLSUtilities.WwiseAssistance public void StopAll() { AkUnitySoundEngine.StopAll(); - TrackedPlayingIDs.Clear(); + _trackedPlayingIDs.Clear(); } } @@ -98,7 +94,7 @@ namespace SLSUtilities.WwiseAssistance { if (playingID != AkUnitySoundEngine.AK_INVALID_PLAYING_ID) { - TrackedPlayingIDs[trackingKey] = playingID; + _trackedPlayingIDs[trackingKey] = playingID; } } @@ -129,7 +125,7 @@ namespace SLSUtilities.WwiseAssistance public static void Pause(string trackingKey, int fadeOutMs = 0) { - if (TrackedPlayingIDs.TryGetValue(trackingKey, out uint playingID)) + if (_trackedPlayingIDs.TryGetValue(trackingKey, out uint playingID)) { AkUnitySoundEngine.ExecuteActionOnPlayingID( AkActionOnEventType.AkActionOnEventType_Pause, @@ -141,7 +137,7 @@ namespace SLSUtilities.WwiseAssistance public static void PauseAllTrackedEvents(int fadeOutMs = 0) { - foreach (var playingID in TrackedPlayingIDs.Values) + foreach (var playingID in _trackedPlayingIDs.Values) { AkUnitySoundEngine.ExecuteActionOnPlayingID( AkActionOnEventType.AkActionOnEventType_Pause, @@ -154,7 +150,7 @@ namespace SLSUtilities.WwiseAssistance // --- 【新增】核心控制:继续 --- public static void Resume(string trackingKey, int fadeInMs = 0) { - if (TrackedPlayingIDs.TryGetValue(trackingKey, out uint playingID)) + if (_trackedPlayingIDs.TryGetValue(trackingKey, out uint playingID)) { AkUnitySoundEngine.ExecuteActionOnPlayingID( AkActionOnEventType.AkActionOnEventType_Resume, @@ -166,7 +162,7 @@ namespace SLSUtilities.WwiseAssistance public static void ResumeAllTrackedEvents(int fadeInMs = 0) { - foreach (var playingID in TrackedPlayingIDs.Values) + foreach (var playingID in _trackedPlayingIDs.Values) { AkUnitySoundEngine.ExecuteActionOnPlayingID( AkActionOnEventType.AkActionOnEventType_Resume, @@ -181,10 +177,10 @@ namespace SLSUtilities.WwiseAssistance /// public static void Stop(string trackingKey, int fadeOutMs = 0) { - if (TrackedPlayingIDs.TryGetValue(trackingKey, out uint playingID)) + if (_trackedPlayingIDs.TryGetValue(trackingKey, out uint playingID)) { Stop(playingID, fadeOutMs); - TrackedPlayingIDs.Remove(trackingKey); + _trackedPlayingIDs.Remove(trackingKey); } } @@ -193,11 +189,11 @@ namespace SLSUtilities.WwiseAssistance /// public static void StopAllTrackedEvents(int fadeOutMs = 0) { - foreach (var playingID in TrackedPlayingIDs.Values) + foreach (var playingID in _trackedPlayingIDs.Values) { Stop(playingID, fadeOutMs); } - TrackedPlayingIDs.Clear(); + _trackedPlayingIDs.Clear(); } } diff --git a/Packages/dev.yarnspinner.unity/.editorconfig b/Packages/dev.yarnspinner.unity/.editorconfig new file mode 100644 index 00000000..a5afb3e6 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.editorconfig @@ -0,0 +1,206 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = lf +insert_final_newline = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +csharp_style_implicit_object_creation_when_type_is_apparent = false +csharp_style_prefer_not_pattern = false +dotnet_style_readonly_field = false +dotnet_style_object_initializer = false + +# RS2008: Enable analyzer release tracking +dotnet_diagnostic.RS2008.severity = none diff --git a/Packages/dev.yarnspinner.unity/.github/FUNDING.yml b/Packages/dev.yarnspinner.unity/.github/FUNDING.yml new file mode 100644 index 00000000..d9e66232 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# These are supported funding model platforms + +patreon: secretlab +github: YarnSpinnerTool +custom: ['https://yarnspinner.itch.io', 'https://assetstore.unity.com/packages/tools/behavior-ai/yarn-spinner-for-unity-the-friendly-dialogue-and-narrative-tool-267061'] diff --git a/Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/bug_report.md b/Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..8288019e --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Create a report to help us improve! +title: '' +labels: bug +assignees: + +--- + +**What is the current behavior?** + + +**Please provide the steps to reproduce, and if possible a minimal demo of the problem**: + + +**What is the expected behavior?** + + +**Please tell us about your environment:** + + - Yarn Spinner Version: + - Unity Version: + +**Other information** + diff --git a/Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/feature_request.md b/Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..4a8c1430 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for Yarn Spinner! +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/Packages/dev.yarnspinner.unity/.github/PULL_REQUEST_TEMPLATE.md b/Packages/dev.yarnspinner.unity/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..d981b6df --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,58 @@ +* **Please check if the pull request fulfills these requirements** +- [ ] Tests for the changes have been added (for bug fixes / features) + - [ ] Does it pass all existing unit tests without modification? + - If not, what did you change? + - If you altered it significantly, what coverage issue did you fix? +- [ ] Docs have been added / updated (for bug fixes / features) +- [ ] CHANGELOG.md has been updated to describe this change + + + + + +* **What kind of change does this pull request introduce?** + +- [ ] Bug Fix +- [ ] Feature +- [ ] Something else + +* **What is the current behavior?** + + + +* **What is the new behavior (if this is a feature change)?** + + + +* **Does this pull request introduce a breaking change?** + + + +* **Other information**: + + diff --git a/Packages/dev.yarnspinner.unity/.github/RELEASE_TEMPLATE.md b/Packages/dev.yarnspinner.unity/.github/RELEASE_TEMPLATE.md new file mode 100644 index 00000000..8303ac22 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/RELEASE_TEMPLATE.md @@ -0,0 +1,38 @@ + + +Yarn Spinner is made possible by your generous patronage. Please consider supporting Yarn Spinner's development by [becoming a patron](https://patreon.com/secretlab), or by buying a copy of Yarn Spinner on [itch.io](https://yarnspinner.itch.io/yarn-spinner) or the [Unity Asset Store](https://assetstore.unity.com/packages/tools/behavior-ai/yarn-spinner-for-unity-267061)! + + + +## 👩‍🚒 Getting Help + +There are several places you can go to get help with Yarn Spinner. + +* Join the [Yarn Spinner Discord](https://discord.gg/yarnspinner). +* Talk to us via [BlueSky](http://bsky.app/profile/yarnspinner.dev) or [Mastodon](https://team.yarnspinner.dev/@yarnspinner) +* To report a bug, [file an issue on GitHub](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues/new?labels=bug+beta&template=bug_report.md&title=). + +## 📦 How To Install Yarn Spinner + +To install the most recent release of Yarn Spinner for Unity, please see the [Installation Instructions](https://docs.yarnspinner.dev/using-yarnspinner-with-unity/installation-and-setup) in the Yarn Spinner documentation. + +If you want to install _this_ particular version of Yarn Spinner for Unity, follow these steps: + +
+Installing Yarn Spinner for Unity {RELEASE_TAG} from Git +

+ +* Open the Window menu, and choose Package Manager. +* If you already have any previous version of the Yarn Spinner package installed, remove it. +* Click the `+` button, and click *Add package from git URL...* +* Enter the following URL: + * `https://github.com/YarnSpinnerTool/YarnSpinner-Unity.git#{RELEASE_TAG}` + +Each release will have a different URL. To upgrade to future versions of Yarn Spinner, you will need to uninstall the package, and reinstall using the new URL. +

+
+ + +## 📜 Changes + + diff --git a/Packages/dev.yarnspinner.unity/.github/get-version.sh b/Packages/dev.yarnspinner.unity/.github/get-version.sh new file mode 100644 index 00000000..e3173839 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/get-version.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Get info about the current commit +most_recent_tag=$(git describe --tags --match="v*" --abbrev=0) +commits_since_tag=$(git rev-list $most_recent_tag..HEAD | wc -l | awk '{$1=$1};1') +sha=$(git log -1 --format=%H) +short_sha=$(git log -1 --format=%h) +branch=$(git rev-parse --abbrev-ref HEAD) + +# A regex for extracting data from a version number: major, minor, patch, +# [prerelease] +REGEX='v(\d+)\.(\d+)\.(\d+)(-.*)?' + +raw_version=${1:-"$most_recent_tag"} + +# Extract the data from the version number +major=$(echo $raw_version | perl -pe "s|$REGEX|\1|" ) +minor=$(echo $raw_version | perl -pe "s|$REGEX|\2|" ) +patch=$(echo $raw_version | perl -pe "s|$REGEX|\3|" ) +prerelease=$(echo $raw_version | perl -pe "s|$REGEX|\4|" ) + +# Calculate the semver from the version (should be the same as the version, but +# just in case) +SemVer="$major.$minor.$patch$prerelease" + +# If there are any commits since the current tag and we aren't overriding our +# version, add that note +if [ "$commits_since_tag" -gt 0 -a -z "$1" ]; then + SemVer="$SemVer+$commits_since_tag" +fi + +# Create the version strings we'll write into the AssemblyInfo files +OutputAssemblyVersion=$(echo "$major.$minor.$patch.$commits_since_tag" | perl -pe "s|\/|\\\/|" ) +OutputAssemblyInformationalVersion=$(echo "$SemVer.Branch.$branch.Sha.$sha" | perl -pe "s|\/|\\\/|" ) +OutputAssemblyFileVersion=$(echo "$major.$minor.$patch.$commits_since_tag" | perl -pe "s|\/|\\\/|" ) + +# Update the AssemblyInfo.cs files +for infoFile in $(find . -name "AssemblyInfo.cs"); do + perl -pi -e "s/AssemblyVersion\(\".*\"\)/AssemblyVersion(\"$OutputAssemblyVersion\")/" $infoFile + perl -pi -e "s/AssemblyInformationalVersion\(\".*\"\)/AssemblyInformationalVersion(\"$OutputAssemblyInformationalVersion\")/" $infoFile + perl -pi -e "s/AssemblyFileVersion\(\".*\"\)/AssemblyFileVersion(\"$OutputAssemblyFileVersion\")/" $infoFile +done + +# If we're running in GitHub Workflows, output our calculated SemVer +if [[ -n $GITHUB_OUTPUT ]]; then + echo "SemVer=$SemVer" >> "$GITHUB_OUTPUT" + echo "ShortSha=$short_sha" >> "$GITHUB_OUTPUT" +fi + +# Log our SemVer +echo $SemVer diff --git a/Packages/dev.yarnspinner.unity/.github/update-package-version.sh b/Packages/dev.yarnspinner.unity/.github/update-package-version.sh new file mode 100644 index 00000000..a1f7cc3d --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/update-package-version.sh @@ -0,0 +1,13 @@ +#/bin/bash + +if [ ! -d ".git" ]; then + echo "This script must be run in the root of the repository." + exit 1 +fi + +VERSION=$(.github/get-version.sh $@) + +jq ".version=\"$VERSION\"" package.json > package.json.tmp +mv package.json.tmp package.json + +echo "Updated package version to $VERSION" diff --git a/Packages/dev.yarnspinner.unity/.github/workflows/broad_version_test.yml b/Packages/dev.yarnspinner.unity/.github/workflows/broad_version_test.yml new file mode 100644 index 00000000..8711c7f9 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/workflows/broad_version_test.yml @@ -0,0 +1,102 @@ +name: Run Tests on Broad Version Range 🌶 + +env: + ACTIONS_RUNNER_DEBUG: true + ACTIONS_STEP_DEBUG: true + +on: + workflow_dispatch: + +jobs: + buildAndTestForSomePlatforms: + concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true # Cancel other jobs if another one arrives + name: Test on ${{ matrix.unityVersion }} for ${{ matrix.targetPlatform }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 1 # Only run one at a time, to prevent license contention + matrix: + projectPath: + - YarnSpinner + unityVersion: + - 2021.3.0f1 + - 2021.3.32f1 + - 2022.1.0f1 + - 2022.1.24f1 + - 2022.2.0f1 + - 2022.2.21f1 + - 2022.3.0f1 + - 2022.3.13f1 + - 2023.1.0f1 + - 2023.1.20f1 + targetPlatform: + # - StandaloneOSX # Build a macOS standalone (Intel 64-bit). + - StandaloneWindows64 # Build a Windows 64-bit standalone. + # - StandaloneLinux64 # Build a Linux 64-bit standalone. + # - iOS # Build an iOS player. + # - Android # Build an Android player. + # - WebGL # WebGL. + steps: + - name: Create empty Unity project + run: | + mkdir -p ${{ matrix.projectPath }}/Assets + mkdir -p ${{ matrix.projectPath }}/ProjectSettings + mkdir -p ${{ matrix.projectPath }}/Packages + + # Add the Unity Input System package, and configure the new project to use + # both the Input System and the legacy Input Manager. + - name: Add Input System package + run: | + cat < ${{ matrix.projectPath }}/ProjectSettings/ProjectSettings.asset + %YAML 1.1 + %TAG !u! tag:unity3d.com,2011: + --- !u!129 &1 + PlayerSettings: + activeInputHandler: 2 + EOF + + cat < ${{ matrix.projectPath }}/Packages/manifest.json + { + "dependencies": { + "com.unity.inputsystem": "1.0.2" + } + } + EOF + + - name: Check out to Packages/YarnSpinner + uses: actions/checkout@v2 + with: + fetch-depth: 0 + path: ${{ matrix.projectPath }}/Packages/YarnSpinner + + - name: Fetch from Cache + uses: actions/cache@v2 + with: + path: ${{ matrix.projectPath }}/Library + key: Library-${{ matrix.projectPath }}-${{ matrix.targetPlatform }}-${{ matrix.unityVersion }}-${{ hashFiles(matrix.projectPath) }} + restore-keys: | + Library-${{ matrix.projectPath }}-${{ matrix.targetPlatform }}-${{ matrix.unityVersion }}- + + - name: Run tests + uses: game-ci/unity-test-runner@v4 + id: testRunner + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + with: + projectPath: ${{ matrix.projectPath }} + unityVersion: ${{ matrix.unityVersion }} + githubToken: ${{ secrets.GITHUB_TOKEN }} + checkName: 'Test Results ${{ matrix.unityVersion }}-${{ matrix.targetPlatform }}' + # customParameters: -quit + + - name: Upload test results + uses: actions/upload-artifact@v2 + if: always() + with: + name: Test results (edit + play, ${{ matrix.unityVersion }}-${{ matrix.targetPlatform }} + # path: ${{ steps.testRunner.outputs.artifactsPath }} + path: artifacts diff --git a/Packages/dev.yarnspinner.unity/.github/workflows/release.yml b/Packages/dev.yarnspinner.unity/.github/workflows/release.yml new file mode 100644 index 00000000..84ba6e63 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Create Release 📦 + +on: + push: + tags: + - "*.*.*" + +jobs: + build: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Extract release notes + id: extract-release-notes + uses: ffurrer2/extract-release-notes@v1 + - name: Read release notes preface + id: release_preface + uses: bluwy/substitute-string-action@v1 + with: + _input-file: .github/RELEASE_TEMPLATE.md + _format-key: '{key}' + RELEASE_TAG: ${{ github.ref_name }} + - name: Create release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: true + prerelease: true + body: | + ${{ steps.release_preface.outputs.result }} + ${{ steps.extract-release-notes.outputs.release_notes }} + diff --git a/Packages/dev.yarnspinner.unity/.github/workflows/test.yml b/Packages/dev.yarnspinner.unity/.github/workflows/test.yml new file mode 100644 index 00000000..6ad20e30 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/workflows/test.yml @@ -0,0 +1,157 @@ +name: Run Tests 🧪 + +env: + ACTIONS_RUNNER_DEBUG: true + ACTIONS_STEP_DEBUG: true + +on: + push: + branches: + - main + - "feature/**" + - "release/**" + paths: + - "Editor/**" + - "Runtime/**" + - "Samples~/**" + - "Tests/**" + workflow_dispatch: + +jobs: + buildAndTestForSomePlatforms: + concurrency: + group: ${{ github.workflow }}-${{ matrix.unityVersion }}-${{ matrix.unityLocalisation }}-${{ matrix.uniTask }} + cancel-in-progress: true # Cancel other jobs if another one arrives + name: ${{ matrix.unityVersion }} (${{ matrix.targetPlatform }}, ${{ matrix.unityLocalisation && 'with unity loc' || 'no unity loc' }}, ${{ matrix.uniTask && 'with unitask' || 'no unitask' }}) + runs-on: [self-hosted, linux] + strategy: + fail-fast: false + # max-parallel: 1 # Only run one at a time, to prevent license contention + matrix: + projectPath: + - YarnSpinner + unityVersion: + - 2022.3.45f1 + - 2023.2.12f1 + - 6000.0.54f1 + - 6000.1.10f1 + unityLocalisation: + - true + - false + uniTask: + - true + - false + targetPlatform: + # - StandaloneOSX # Build a macOS standalone (Intel 64-bit). + # - StandaloneWindows64 # Build a Windows 64-bit standalone. + - StandaloneLinux64 # Build a Linux 64-bit standalone. + # - iOS # Build an iOS player. + # - Android # Build an Android player. + # - WebGL # WebGL. + steps: + - name: Create empty Unity project + shell: bash + run: | + mkdir -p ${{ matrix.projectPath }}/Assets + mkdir -p ${{ matrix.projectPath }}/ProjectSettings + mkdir -p ${{ matrix.projectPath }}/Packages + mkdir -p output + + # Add the Unity Input System package, and configure the new project to use + # both the Input System and the legacy Input Manager. + - name: Add Input System package + shell: bash + run: | + cat < ${{ matrix.projectPath }}/ProjectSettings/ProjectSettings.asset + %YAML 1.1 + %TAG !u! tag:unity3d.com,2011: + --- !u!129 &1 + PlayerSettings: + activeInputHandler: 2 + EOF + + cat < ${{ matrix.projectPath }}/Packages/manifest.json + { + "dependencies": { + "com.unity.inputsystem": "1.11.2" + } + } + EOF + + # Select correct TMP Essentials package + if [[ ${{matrix.unityVersion}} == "2022"* ]]; then + TMP_VERSION="ugui-1.0.0" + else + TMP_VERSION="ugui-2.0.0" + fi + echo "Installing TMP Essentials for $TMP_VERSION" + + # Add the correct version of the TMP Essentials package to package manifest + MANIFEST_PATH=${{ matrix.projectPath }}/Packages/manifest.json + jq ".dependencies += {\"dev.yarnspinner.tmp-essentials\": \"https://github.com/desplesda/dev.yarnspinner.tmp-essentials.git#$TMP_VERSION\"}" "$MANIFEST_PATH" > manifest.json + mv manifest.json "$MANIFEST_PATH" + + - name: Add Unity Localisation + if: ${{ matrix.unityLocalisation }} + run: | + # Add Unity Localisation package to package manifest + MANIFEST_PATH=${{ matrix.projectPath }}/Packages/manifest.json + jq '.dependencies += {"com.unity.localization": "1.3.2"}' "$MANIFEST_PATH" > manifest.json + mv manifest.json "$MANIFEST_PATH" + + - name: Add UniTask Package + if: ${{ matrix.uniTask }} + run: | + # Add UniTask package to package manifest + MANIFEST_PATH=${{ matrix.projectPath }}/Packages/manifest.json + jq '.dependencies += {"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"}' "$MANIFEST_PATH" > manifest.json + mv manifest.json "$MANIFEST_PATH" + + - name: Check out to Packages/YarnSpinner + uses: actions/checkout@v2 + with: + fetch-depth: 0 + path: ${{ matrix.projectPath }}/Packages/dev.yarnspinner.unity + + - name: Run edit mode tests + run: | + docker run \ + --rm \ + -v ./${{ matrix.projectPath }}:/project \ + -v ./output:/output \ + -e TEST_MODE=EditMode \ + --hostname YS-Linux-Build \ + yarnspinner/unity-${{ matrix.unityVersion }} + + - name: Run play mode tests + if: always() + run: | + docker run \ + --rm \ + -v ./${{ matrix.projectPath }}:/project \ + -v ./output:/output \ + -e TEST_MODE=PlayMode \ + --hostname YS-Linux-Build \ + yarnspinner/unity-${{ matrix.unityVersion }} + + - name: Generate HTML test report (Play Mode) + uses: rjtngit/nunit-html-action@v1 + if: always() + with: + inputXmlPath: output/TestResults-PlayMode.xml + outputHtmlPath: output/TestResults-PlayMode.html + + - name: Generate HTML test report (Edit Mode) + uses: rjtngit/nunit-html-action@v1 + if: always() + with: + inputXmlPath: output/TestResults-EditMode.xml + outputHtmlPath: output/TestResults-EditMode.html + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: Test results (edit + play, ${{ matrix.unityVersion }} ${{ matrix.targetPlatform }} ${{ matrix.unityLocalisation && 'with-unity-loc' || 'no-unity-loc' }} ${{ matrix.uniTask && 'with-unitask' || 'no-unitask' }}) + # path: ${{ steps.testRunner.outputs.artifactsPath }} + path: ./output diff --git a/Packages/dev.yarnspinner.unity/.github/workflows/update_dlls.yml b/Packages/dev.yarnspinner.unity/.github/workflows/update_dlls.yml new file mode 100644 index 00000000..773805ee --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.github/workflows/update_dlls.yml @@ -0,0 +1,98 @@ +name: Update DLLs 📚 + +on: + workflow_dispatch: + +jobs: + update_dlls: + name: Update Yarn Spinner DLLs + runs-on: ubuntu-latest + permissions: + # We need to be able to: + # 1. create a branch in a repo ('contents'), and + # 2. create a pull request using that branch ('pull-requests') + pull-requests: write + contents: write + + steps: + - name: Checkout Yarn Spinner for Unity + uses: actions/checkout@v2 + with: + path: YarnSpinner-Unity + - name: Checkout Yarn Spinner + uses: actions/checkout@v2 + with: + repository: YarnSpinnerTool/YarnSpinner + path: YarnSpinner + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + 9.0.x + + - name: Fetch all commits + run: git fetch --unshallow + working-directory: ./YarnSpinner + + - name: Install dotnet-assembly-alias + run: dotnet tool install -g Alias + + # Update the assembly info for this build of YS, so that the About window is + # appropriate + - name: Execute GitVersion + id: version # step id used as reference for output values + run: ./get-version.sh + working-directory: ./YarnSpinner + + - name: Restore dependencies + run: dotnet restore + working-directory: ./YarnSpinner + + - name: Build + run: dotnet build --no-restore --configuration Release + working-directory: ./YarnSpinner + + # Don't proceed unless we're including a build of Yarn Spinner that passes + # its tests. + - name: Test + run: dotnet test --no-build --configuration Release --verbosity normal + working-directory: ./YarnSpinner + + # We need to copy dependency DLLs into the project, but if a Unity project + # contains multiple DLLs with the same name (even from a package), that's an + # error. This causes problems for users who want to use, for example, + # Google.Protobuf (especially if they want to use a different version). + # + # Our solution partly involves renaming the dependency DLLs to have the + # prefix 'Yarn.', and updating all references to these renamed DLLs, using + # dotnet-assembly-alias + # (https://github.com/getsentry/dotnet-assembly-alias/). For more + # information on this fix, see + # https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues/15#issuecomment-1036162152. + - name: Rename vendored DLLs + run: | + assemblyalias --target-directory "YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/" --prefix "Yarn." --assemblies-to-alias "Antlr*;Csv*;Google*;" + assemblyalias --target-directory "YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/" --internalize --prefix "Yarn." --assemblies-to-alias "System*;Microsoft.Bcl*;Microsoft.Extensions*" + + # Copy all of the dependency DLLs into the YarnSpinner-Unity repo, except + # for Microsoft.CSharp.dll (which is provided by Unity, so including it + # would cause an error.) + - name: Copy DLLs + run: | + cp -v YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/*.dll YarnSpinner-Unity/Runtime/DLLs + cp -v YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/*.pdb YarnSpinner-Unity/Runtime/DLLs + cp -v YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/*.xml YarnSpinner-Unity/Runtime/DLLs + rm -fv YarnSpinner-Unity/Runtime/DLLs/Microsoft.CSharp.dll + + # Make the PR against YarnSpinner-Unity that merges this change + - name: Create pull request + uses: peter-evans/create-pull-request@v3 + with: + path: ./YarnSpinner-Unity + commit-message: Update Yarn Spinner DLLs to YarnSpinnerTool/YarnSpinner@${{ steps.version.outputs.ShortSha }} + branch: update-dlls-${{ steps.version.outputs.ShortSha }} + title: Update Yarn Spinner DLLs to latest (${{ steps.version.outputs.ShortSha }}) + body: | + This is an automated PR made by @${{ github.actor }} that updates the precompiled Yarn Spinner DLLs (and their dependencies) to YarnSpinnerTool/YarnSpinner@${{ steps.version.outputs.ShortSha }} (v${{ steps.version.outputs.SemVer }}). diff --git a/Packages/dev.yarnspinner.unity/.pkgignore b/Packages/dev.yarnspinner.unity/.pkgignore new file mode 100644 index 00000000..410cc732 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/.pkgignore @@ -0,0 +1,21 @@ +# Ignore the Samples symlink, which links to Samples~. This symlink exists +# so that the distributed package ships its samples in a folder called +# Samples~ (which Unity will ignore, and users can import into their projects +# when they want to), while the sameples are visible in the package when +# developing. +Samples +Samples.meta + +# Ignore markdown files when packaging for asset stores - they'll be converted +# into PDFs or other more readable formats. +*.md +*.md.meta + +# Ignore test files, which don't need to be shipped (they're not needed by +# end-users) +Tests +Tests.meta + +# Ignore the file that defines which way we install the samples - the +# appropriate setting will be manually copied in. +YarnPackageImporter.SamplesInstallApproach.cs diff --git a/Packages/dev.yarnspinner.unity/CHANGELOG.md b/Packages/dev.yarnspinner.unity/CHANGELOG.md new file mode 100644 index 00000000..31820158 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CHANGELOG.md @@ -0,0 +1,1258 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +### Added + +### Changed + +### Removed + +## [3.2.1] 2026-03-27 + +### Changed + +- Fixed an issue in Unity 6.4 where newly created Yarn scripts could fail to import correctly. + +## [3.2.0] 2026-03-27 + +### Added + +- When using the Unity Localization package Yarn Spinner can now sort the localisation tables based on a lines position in the file. + - This resolves an issue where lines added into the middle of a Yarn file. However, this does require sorting your localisation table every time you edit your Yarn files. + - This setting defaults to off, and can be enabled in Yarn Spinners settings in `Edit -> Project Settings -> Yarn Spinner`. + - When sorting, any values found in the localisation table that didn't come from your Yarn files will be logged, and sorted to the top of the table. +- `YarnSpinnerAssemblyGeneratedYSLSPath` function to the `YarnSpinnerProjectSettings` so that each assembly can have a generated and consistent path. +- Generated YSLS files now include subtype information for instance commands. + - This means a command like `<>` now knows that the `"gary"` is the name of a game object, and the specific game object subclass. + - This only works at a single level of MonoBehaviour depth; subclasses of a `MonoBehaviour` subclass will not be recognised. +- `YarnNodeParameterAttribute` can now be added to string parameters in function and command methods + - This is intended to allow you to hint to the VSCode extension that, when writing this parameter in commands and functions, it should limit suggestions to nodes. +- `YarnEnumParameterAttribute` can now be added to parameters in functions and command methods. + - This has one field, which is the name the enum as declared in your Yarn. + - This is intended to allow you to hint to the VSCode extension to offer enum based suggestions. This is a temporary solution, until forward declaration of Yarn enums into C# works. +- Incorrectly defined parameters for commands and functions will now generate diagnostics on the C# implementation, warnings exist for: + - invalid types + - incorrectly attributed parameters +- `ContentDidDismiss` call to the `IAsyncTypewriter` to let typewriters know they are safe to do any cleanup +- A `TMP_Text` property called `TextElement` to the `IAsyncTypewriter` + - this is optional but most of the time custom typewriters will need to get the main text element anyway so might as well just have it as something they are given at the start + +### Changed + +- `YarnProjectImporter` now updates asset addresses and generates the C# variable storage class _after_ the project import completes, rather than during the import. +- Each assembly now gets its own YSLS file when generating them. + - We renamed the test `.asmdef` files from `YarnSpinnerTests.x` to `YarnSpinner.Unity.Tests.X`, so that they are matched by the existing filters. +- Generated a YSLS file now matches the newer schema. See the [Yarn Spinner core repo](https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/YarnSpinner.LanguageServer/src/Server/Documentation/ysls.schema.json) for details of this. +- When matching assets to line IDs, the importer now prefers exact matches to the line ID. +- `YarnProjectImporterEditor` now resets some internal state when creating its UI, which should help avoid a bug where the list of sources would grow over time. +- Added explicit registration for `UnityEngine.Awaitable`, and the missing cases for `System.Threading.Tasks.Task` commands, fixing issues where some times certain specific configurations would fail to register the command. + - As part of this, we also made it so Commands can now be registered with up to 16 parameters. + - Please don't actually register a command with 16 parameters. +- Fixed a bug where the InterfaceContainer would sometimes lose it's connection. + - unsure why this happens, just Unity things™ +- Line Presenter now uses the `InterfaceContainer` for custom typewriters. +- `.ysls.json` file generation is now on by default. +- Yarn Spinner for Unity now works with `EntityId` objects instead of instance ID integers in Unity 6.4 and later. + +## [3.1.4] 2025-12-19 + +### Changed + +- Fixed an issue that caused build errors when using the Text Animator add-on. + +## [3.1.3] 2025-12-09 + +### Changed + +- Fixed an issue that caused OpenUPM downloads to fail to compile. + +## [3.1.1] 2025-12-03 + +### Overview + +Yarn Spinner for Unity 3.1 contains a number of improvements and useful changes. + +#### Dialogue Runner Is Now More Async + +The dialogue runner's `StartDialogue` and `Stop` methods are now `async`, and return a task. When you call `StartDialogue`, you'll receive a task (either a `System.Threading.Tasks` task, a `UnityEngine.Awaitable`, or a `UniTask`) that will complete after every dialogue presenter has finished running its `OnDialogueStartedAsync` method. Likewise, the `Stop` method will finish after every dialogue presenter has finished running its `OnDialogueCompleteAsync` method. This is really useful for making sure that you don't accidentally make a change to your scene in the middle of your dialogue presenters getting ready. + +#### Dialogue Option Fallthrough + +In Yarn Spinner, you can add a condition to the end of an option. If the condition evaluates to `false`, Yarn Spinner will mark the option as "unavailable". It's up to your game to decide what that means - you might want to make the option visible but unselectable, or you might want to hide the option from the player entirely. However, if _every_ option is unavailable, the player has no option they could select. Previously, this could cause your game to have to soft-lock the player, since they weren't able to proceed. + +In Yarn Spinner 3.1, dialogue presenters are now allowed to tell the Dialogue Runner that no option was selected at all. When this happens, Yarn Spinner will skip the options and move on to the next part of the script: + +``` +Guard: Who goes there? + +// If the player is a thief, a royal visitor, or a merchant, then +// go run the appropriate conversation for that. The player might be +// some combination of the three, so let them choose. +-> A thief! <> + <> +-> A royal visitor! <> + <> +-> A merchant! <> + <> + +// But if the player is NONE of those, then none of the options would have +// been available. We'll fall through to here. + +Player: I'm nobody! +<> +``` + +> [!NOTE] +> You can turn off this behaviour by setting the `allowOptionFallthrough` property on your `DialogueRunner` to `false`. + +#### Lines Know Where They Came From + +When Yarn Spinner sends a line to your game, it wraps up the line in an object called a [`LocalizedLine`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.localizedline). Previously, if your game has multiple Dialogue Runners that are running at the same time, it wasn't possible to know which runner the line came from. In Yarn Spinner 3.1, the `LocalizedLine` now has a [`Source`](https://docs.yarnspinner.dev/3.1/api/csharp/yarn.unity/yarn.unity.localizedline/yarn.unity.localizedline.source) property that tells you where it came from. + +#### Options Can Now Be Hurried Up And Cancelled + +Just like how lines have a separate 'hurry up' and 'next' cancellation tokens that act as a signal to move things along, options now have the same 'hurry up' and 'next' tokens. (Previously, they only had a single cancellation token that signalled that option selection was no longer necessary.) This allows your game to signal that you want to hurry up the presentation of your dialogue's options. + +#### New Typewriter System + +We've updated the way that typewriters are used in the built-in Line Presenter system, to make it easier to customise. This is useful for when you want to take full control over how the line appears over time, and for when you want to have in-game events occur (like sound effects) as the line appears. + +To create a custom typewriter, create a class that implements [`IAsyncTypewriter`](https://docs.yarnspinner.dev/3.1/api/csharp/yarn.unity/yarn.unity.iasynctypewriter). You can find an example of how to write a custom typewriter in the source code for the [`LetterTypewriter`](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/blob/main/Runtime/Views/Typewriter/LetterTypewriter.cs) class. + +> [!NOTE] +> As part of this change, the On Character Typed event on Line Presenter has been removed. If you want to run code every time a character appears, create a new script that subclasses from [ActionMarkupHandler](https://docs.yarnspinner.dev/3.1/api/csharp/yarn.unity/yarn.unity.actionmarkuphandler), and add that to an object in your scene. Add that object to the Line Presenter's "Event Handlers" list. In your ActionMarkupHandler subclass, you can write code that gets called every time characters appear on screen by implementing the [OnCharacterWillAppear method](https://docs.yarnspinner.dev/3.1/api/csharp/yarn.unity/yarn.unity.actionmarkuphandler/yarn.unity.actionmarkuphandler.oncharacterwillappear). + +#### Removed Legacy `DialogueView` Classes + +Yarn Spinner 3.0 introduced a new programming model for presenting dialogue, called [Dialogue Presenters](https://docs.yarnspinner.dev/3.1/components/dialogue-view). As part of the rollout of this new API, we made the old `DialogueView` class act as a compatibility layer, and marked it as deprecated. Yarn Spinner 3.1 removes this deprecated code. If you have code that started life as a Yarn Spinner 2.0 project, you will need to [migrate your legacy dialogue presentation UI code to use Dialogue Presenters](https://docs.yarnspinner.dev/3.1/changelog/upgrading-from-yarn-spinner-2#dialogueviewbase-is-now-dialoguepresenter) by changing their parent class to `DialoguePresenter`, and implementing the new methods for presenting lines and options + +### Added + +- `DialogueRunner` will now log warnings if a dialogue presenter throws an `OperationCanceledException` - these usually indicate that a task they were themselves waiting on was cancelled, and that the presenter didn't clean up. +- The generated `.ysls.json` file now contains more complete command and function information. +- Added a new attribute for Yarn Spinner editors: `LabelFrom` allows specifying a dynamic label for a property by invoking a method. +- Added multiple typewriters and support for them on the `LinePresenter`: + - By Word + - By Letter + - Instantly +- Added a virtual `IAsyncTypewriter` field `Typewriter` onto `DialoguePresenterBase` +- Added new virtual `OnNodeEnter` and `OnNodeExit` calls onto `DialoguePresenterBase`. +- Added a static `FindRunner` call onto `DialogueRunner`, which first tries to find it on the game object itself, followed by anywhere in the scene. +- Added a `Source` field to the `LocalizedLine`, this can be of any type but by default will be the `DialogueRunner` that caused the line to be created +- Options can now be hurried up, the same as lines + - added `RunOptionsAsync(DialogueOption[], LineCancellationToken)` to `DialoguePresenterBase`. + - this method is virtual and is now the recommended way to respond to options + - the default implementation just returns nothing selected + - added `NextContentToken` to `LineCancellationToken` which mirrors the existing `NextLineToken`. + - added `IsNextContentRequested` to `LineCancellationToken` which mirrors the existing `IsNextLineRequested` bool. + - added `RequestHurryUpOption` to `DialogueRunner`. + - added hurry up option inputs to `LineAdvancer` +- `LineAdvancer` now better handles situations where you want to use the same input for hurrying up and skipping lines. + - This behaviour is controllable via the `separateHurryUpAndAdvanceControls` field +- Added a new container type `InterfaceContainer`, a wrapper class to clean up some interface serialisation pains + +### Changed + +- Fixed a compiler error that occurred when the Unity Input System was enabled, but not actually installed. +- Fixed an error where calls to `DialogueRunner.AddFunction()` in methods that are in nested classes would cause the function to be registered multiple times, leading to compiler failures. +- Fixed a bug where versions of Unity prior to Unity 2022.3.33 had compilation errors around packages. +- Fixed a bug where some inspector property fields weren't bound preventing configuring Unity localisation or addressables in Unity 2022.3. +- Added more specificity to the `Analyser` class's use of the C# code generation API. +- `AddLineTagsToFilesInYarnProject` is now `public`, allowing for easier use of it in editor scripting. +- Showing character name behaviour in the `LinePresenter` now correctly matches the documentation. +- `DialogueRunner.StartDialogue` is now asynchronous and waits for all presenters to finish their `OnDialogueStartedAsync` calls before exiting +- `DialogueRunner.Stop` is now asynchronous and waits for all presenters to finish their `OnDialogueCompleteAsync` calls before exiting +- Fixed a bug where the continue button on the `LinePresenter` could become non interactive. +- `PaletteMarkerProcessor` and `StyleMarkerProcessor` now look less aggressively for their Dialogue Runner. +- `LocalizedLine.TextWithoutCharacterName` still does not contain the text of the character, but does contain the Markup attribute of the character. + - This allows other parts of your dialogue UI that might need to know who said a line still use the `TextWithoutCharacterName` property. +- `ReplacementMarkuphandler` now returns a `ReplacementMarkerResult` instead of a list of diagnostics. + - This is to match the behaviour change in core to fix a markup offset bug +- `OptionsPresenter` now returns null if there are no options that can be selected due to their availability. +- If there are no available options, Dialogue Runner will now fallthrough to the next piece of content + - This behaviour can be disabled in the `allowOptionFallthrough` field on the runner. +- Fixed an issue where Builtin localisation would always use the base localisation when fetching a localised asset ([#344](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues/344)) +- The UPM samples installer now installs from a specific tag, rather than the head of the repo. +- Options can now be hurried up, the same as lines. This necessitated several obsolences: + - `RunOptionsAsync(DialogueOption[], CancellationToken)` is now obsolete. + - `IsNextLineRequested` is now obsolete. + - `NextLineToken` is now obsolete. +- `RunOptionsAsync(DialogueOption[], CancellationToken)` is now virtual. + - the default implementation selects nothing and instantly returns. +- The Dialogue Presenter template file now has the newer form of `RunOptionsAsync`. +- Built in presenters now use the newer form of `RunOptionsAsync`. +- The `InputSystemAvailability` static class has been made public. +- `LineAdvancer` will now ignore the signal to 'hurry up' if it comes in the same frame as line was requested to be shown. + - This fixes a bug where the same key was used to start conversation as well hurry up dialogue. +- Fixed an issue on `EnsureInputModuleAvailable` where, if there was an input system in the scene but it was of the wrong type, the console would fill up with errors. +- `DialogueRunner` now waits one frame before starting dialogue when automatically start dialogue is turned on. + +### Removed + +- Removed `ActionMarkupHandlers` list from `DialoguePresenterBase`. This was only used by the default line presenter, and is now handled by the typewriter system. This which is more representative of what Action Markup Handling already entailed. +- Removed the legacy Dialogue Views, Typewriter, and Effects. +- Removed the `ReplacementMarkupHandler.NoDiagnostics` static property, as this no longer matches any need due to core changes around replacement markup. +- Removed `RunOptionsAsync(DialogueOption[], CancellationToken)` from `LinePresenter`. This method is now virtual, and the default implementation does what we needed here. + +## [3.0.3] 2025-06-21 + +### Added + +- Added a `[br /]` markup tag into the common markers default markup palette. + - This will be translated into a TextMeshPro `
` marker. +- Line Advancer will now fall back to using key codes if Input Actions are selected, but Unity Input System is not available. +- The Dialogue System prefab now creates a UI input module that matches your current input system when it's added to the scene, rather than baking in a specific input module. + +### Changed + +- Fixed a bug where `LinePresenter` would not run all registered action markup handlers during a typewriter effect. +- Fixed an issue where the default Dialogue Runner prefab didn't have its `LineAdvancer` component configured correctly. +- Renamed `actionMarkupHandlers` to be `eventHandlers` on the `LinePresenter`, this fixes an easy to occur typo with the base classes `ActionMarkupHandlers`. +- Fixed an issue where Line Advancers set to use Key Codes would not work when the Legacy Input Manager is not available. + +## [3.0.2] 2025-06-13 + +### Added + +- The inspector for the Dialogue Runner now has a drop down for selecting the saliency strategy. +- `VariableStorageBehaviour` now supports adding a variable change listener for _all_ variables. + +### Changed + +- The Yarn Spinner header in the Inspector no longer shows `` markup in versions of Unity prior to Unity 6. +- `LinePresenter` now implements its typewriter effect with `BasicTypewriter`, an implementation of the `IAsyncTypewriter` interface. +- The typewriter effect in `LinePresenter` is now framerate-independent. +- Fixed an issue in UnityLocalisedLineProvider where an exception would be thrown if an asset table was not provided. + +### Removed + +- `SerializableDictionary` no longer exposes non-generic `IDictionary` methods; instead, all operations are now correctly type-checked. + +## [3.0.0] 2025-05-16 + +### Added + +- A basic saliency sample to show off the basics and how to get started with storylets. +- Logo and docs/help links to custom editors in Yarn Project (standalone), Yarn Script (standalone), and Dialogue Runner (component when on GameObject). +- Description of package samples which summarises Samples therein. +- Updated a few out-of-date HelpUrl values which referenced v1 docs. +- Default Yarn Project exlude patterns updated to ignore the standard samples folders +- Functions that are registered using the `AddFunction` method are now supported by the Yarn Spinner compiler. +- YarnPackageImporter now has an install samples menu item in `Window -> Yarn Spinner -> Install Samples` +- 'Incorrect number of parameters' error messages when running commands will now show more detailed error messages if available. +- Yarn scripts can now directly link to vscode for situations where you want to use that as your Yarn editor but have another editor as your C# editor. +- `UnityLocalisedLineProvider` now supports [fallback locales](https://docs.unity3d.com/Packages/com.unity.localization@1.5/manual/Locale.html#fallbacks). +- `UnityLocalisedLineProvider` now supports shadow lines. +- `BuiltinLocalisedLineProvider` now supports falling back to different locale. +- `DialogueRunner.DialogueTask` now completes after all dialogue presenters finish their `OnDialogueComplete` method call, rather than before. +- The `DialogueRunner.onDialogueComplete` event is now invoked after all dialogue presenters finish their `OnDialogueComplete` method call, rather than before. + +### Changed + +- Fixed an error that caused the YarnProject asset editor to incorrectly report a compile error when there wasn't one. +- Certain errors thrown when a command can't be run now show the full text of the command, not just the command name. +- The default Yarn Spinner Script template is now no longer an empty node. +- Renamed 'Dialogue View Script' menu item to 'Dialogue Presenter Script'. +- Added `DialogueRunner.DialoguePresenters` (and marked `DialogueRunner.DialogueViews` as obsolete.) +- Fixed a lot of missing null checks +- Fixed a bug in SampleRenderDetector that caused build errors. +- Menu for creating a prefab dialogue system now says "Dialogue System" instead of "Dialogue Runner" +- Line Advancer will now advance to the next line if the line presenter is finished presentation and is awaiting input and the player sends the hurry up action + - this only happens when the default line presenter is the one presenting the line + - other presenters don't really have an obvious "I am done showing, but not done presenting" concept as the line presenter does + - adopting this behaviour into other presenters is not difficult, just not something that can be done automatically + +### Removed + +- SaveStateToPlayerPrefs method +- LoadStateFromPlayerPrefs method +- `LinesAvailable` property from line providers, it no longer makes sense in an async world +- `baseLayer` property on Simple Character as it was no longer used +- Obsolete uxml elements on `LanguagePopup.cs` +- All samples, they now live in their own repository + + +## [3.0.0-beta2] 2025-04-09 + +### Added + +- Yarn action handling now supports a wider range of return values. +- `YarnImporter.GetHashString()` is now public. +- `UnityLocalisedLineProvider` no longer throws an exception if an asset table is provided but does not contain an asset for a line. +- `DialogueRunner.CommandDispatcher` is now set up on first access, rather than in `Awake`. + - This allows other objects to work with the command dispatcher (for example, registering new methods) in their `Awake` methods, even if their `Awake` methods run before `DialogueRunner`'s. +- `YarnCommand` and `YarnFunction` commands now allow including `.` characters in their names. +- Fixed an issue in SerializableDictionary.cs that caused builds to fail. +- `DialogueRunner.AddCommandHandler` and `DialogueRunner.AddFunction` now validate that the provided names contain no spaces. +- `DialogueRunner.AddCommandHandler` now supports methods whose last parameter is an array of values. + - This allows for commands with a variable list of parameters. For example, consider the following method: + ```csharp + void LogStrings(int a, string[] remainder) { + Debug.Log($"a = {a}, remainder={string.Join(",", remainder)}"); + } + ``` + This method can be registered as a Yarn command: + ```csharp + dialogueRunner.AddCommandHandler("my_command", LogStrings); + ``` + And called from Yarn Spinner: + ``` + // logs "a = 42, remainder=this,is,pretty,great" + <> + ``` + > [!NOTE] + > Array parameters are required to be the last parameter of the method. +- Variable storage objects now allow registering a 'change listener' that runs when a variable is changed. + - To add a change listener, call `AddChangeListener` on your variable storage, and provide the name of the variable you want to watch for changes for and a delegate that should run when the variable changes: + ```csharp + VariableStorageBehaviour storage = // ... + + var changeListener = storage.AddChangeListener("$myVariable", (newValue) => { + Debug.Log("$myVariable changed to " + newValue); + }); + ``` + You can remove a change listener by disposing it: + ```csharp + changeListener.Dispose(); + ``` + Change listeners can't be added for smart variables. If you add a change listener for a stored variable (that is, one declared in the Yarn script), the change listener's parameters must match the type of the stored variable. + + > [!NOTE] + > If you're implementing your own subclass of `VariableStorageBehaviour`, your `SetValue` methods must call `NotifyVariableChanged` to notify any registered change listeners. + > ```csharp + > public override void SetValue(string variableName, string newValue) + > { + > // ... existing behaviour ... + > + > NotifyVariableChanged(variableName, newValue); + > } + > ``` +- Added a sample (CustomSaliency) showing off making a custom saliency selection strategy. +- Added a sample (InlineEvents) showing off using `ActionMarkupHandler`'s to perform in line events. +- Added a sample (Replacement Markers) showing off various different approaches to performing marker replacement. +- The Dialogue System prefab now has support for some common rich text tags + - `[i]` will italicise text + - `[b]` will embolden text + - `[u]` will underline text + - `[s]` will strike through text +- `AsyncOptionsView` now supports configuring its fade duration. +- `MarkupPalette` and the associated `PaletteMarkerProcessor` now support custom TMP rich text tags beyond what the more basic markers allow. +- `LineMetadata` now has public API methods for constructing and manually adding elements. +- `DialogueRunner.SetProject` now sets the `Program` of its internal `Dialogue` object. Previously, this didn't happen until `StartDialogue()`. +- Added a sample (Advanced Saliency) making use of templated nodes and built in saliency to show off creating storylet vignettes. +- `IActionMarkupHandler` interface which the `ActionMarkupHandler` monobehaviour now conforms to +- Action markup handlers now has a `OnLineWillDismiss` method which is called right before the line view fades itself away. +- `LinePresenterButtonHandler` is a new `ActionMarkupHandler` subclass that manages the continue button on the line view. + +### Changed + +- Fixed an issue where, on Windows, projects would fail to automatically update when a file that belonged to them was created or edited. +- Fixed an issue where Unity Localization `rid` values would change on reimport when they didn't have to. +- Fixed an issue where a `[pause/]` marker at the start of the line would cause all pauses to not work ([@iatenothingbutriceforthreedays](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/pull/291)) +- Inspector-exposed fields on `LineView` are now public. +- Fixed an issue where a `.meta` file was causing warnings to appear in Unity on import. ([@Colbydude](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/pull/294)) +- Fixed an issue where functions would not be registered with the VM until after the first call to `CommandDispatcher`. +- `YarnProjectImporter.GenerateStringsTable` is now public. +- Yarn Projects now allow choosing more specific cultures (for example 'pt-BR' and 'en-AU' rather than simply 'pt' and 'en') as their base language. +- `DialogueRunner.dialogueCompleteTask` now uses a completion source, rather than polling. +- `YarnNode` dropdowns no longer show individual node group members in the list. +- `IActionRegistration` now correctly supports commands without parameters. +- Renamed `TemporalMarkupProcessor` to `ActionMarkupHandler`. +- Renamed `AttributeMarkerProcessor` to `ReplacementMarkupHandler` +- When using Unity Localization, Yarn Projects now populate the string table collection using a post-processor, rather that in the middle of import. This should fix errors that would occasionally cause string tables to not correctly update when a Yarn file changes. +- Fixed an issue where the System.Threading.Tasks implementation of YarnTask.WaitUntil did not return early when the CancellationToken was cancelled ([@dogboydog](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/pull/304)) +- If a `VoiceOverView` is configured to automatically advance at the end of audio playback, when a line that has no `AudioClip` is delivered, the `VoiceOverView` will now advance the line immediately after logging an error. +- Renamed `FormatForMarker` to `PaletteForMarker` in `MarkupPalette`. +- Generated variable storage code now represents string values as signed integers, not unsigned integers. (Unity doesn't correctly serialize enum values over the signed integer maximum.) +- `YarnNode`-attributed variables can now be configured to allow editing as a text field even when the Yarn Project is not set. This is now the default behaviour for `DialogueReference`. +- `sourceFilePaths` on `ProjectImportData` renamed to be `sourceFilePatterns` because that is what it actually is. +- Fixed a crash in project import when externally declared invalid functions had no associated file with the error +- When an error that `YarnProjectImportEditor` needs to show has no file associated with it the editor no longer draws an empty TextAsset field. +- Heavily reworked the structure of the project folders. +- Renamed `AsyncDialogueViewBase` to be `DialoguePresenter` + - Renamed `AsyncLineView` to be `LinePresenter` + - Renamed `AsyncOptionItem` to be `OptionItem` + - Renamed `AsyncOptionsView` to be `OptionsPresenter` + - Renamed `VoiceOverView` to be `VoiceOverPresenter` +- `LanguageAttribute` and `YarnNodeAttribute` are now in the `Yarn.Unity.Attributes` namespace. +- Facial expressions on the NPCs now have more common names. +- `MoveEvent.cs` now uses YarnTasks instead of `Awaitable` +- `ActionMarkupHandler` now conforms to the `IActionMarkupHandler` interface +- `LinePresenter` now has a public list of `IActionMarkupHandler` for non-monobehaviour based markup handling. +- `PauseEventProcessor` is now just an `IActionMarkupHandler` and no longer a monobehaviour. +- Fixed a bug that could cause multiple option items to be selected at once. +- The awaitable version of `YarnTask.Delay` no longer throws an `OperationCancelledException` when cancelled. + + +### Removed + +- `YarnProject.GetHeaders` is now deprecated, in favour of `DialogueRunner.Dialogue.GetHeaders`. +- Removed `TypewriterHandler`; this behaviour is now intrinsic to the `AsyncLineView`. +- Removed the `tags` header from the template new yarn file as it is no longer the best way to add metadata headers into a node. +- Removed Continue Button support from the `LinePresenter` + +## [3.0.0-beta1] 2024-11-30 + +### Added + +- Updated voiceover and translation credits for the Intro sample scene. +- Added shadow line support to BuiltInLineProvider. +- Added support for generating C# variable storage classes that expose properties for string, number and boolean variables found in a Yarn Project. +- `YarnCommand` methods may now use `params` array parameters. +- Asynchronous Default View and Prefab: + - `AsyncLineView` is intended as a full replacement for `LineView`. + - `AsyncOptionsView` is intended as a full replacement for `OptionsListView`. + - `AsyncOptionItem` is intended as a full replacement for `OptionView`. + - `LineAdvancer` is a replacement for `DialogueAdvanceInput`. + - `Async Dialogue System` prefab is intended as a full replacement for `Dialogue System` prefab. +- New approach to handling replacement markup: + - `AttributeMarkerProcessor` defines the required fields and methods to create replacement markup processors. + - `PaletteMarkerProcessor` implements `MarkupPalletes` replacement. + - `StyleMarkerProcessor` implements TMP style tag replacement. +- New approach to handling display-time markup: + - `TemporalMarkupHandler` defines the required fields and methods to create markup handlers. + - Typewriter is now implemented as a markup handler inside `TypewriterHandler`. +- `LineCancellationToken` is a combination of two cancellation tokens + - this allows for the highly common model of asking a line to hurry up vs skip + - New method `HurryUpCurrentLine` on `DialogueRunner` is how to trigger this. +- Async form of `FadeAlpha` added to `Effects`. +- Cancellable `Delay` added to `AsyncHelpers`. +- Cancellable `WaitUntil` that uses a predicate added to `AsyncHelpers`. +- Localization assets can now be created as external assets, and provided to the Yarn Project importer. + - This can be useful if you need to edit the contents of your Localization assets, rather than letting Yarn Spinner create and manage them for you. +- You can now create a new Dialogue View script by opening Assets -> Create -> Yarn Spinner -> Dialogue View Script. This will create a new C# file that contains an empty template for building your own Dialogue View. + +### Changed + +- Line Providers are now responsible for performing markup parsing + - for the most part this will done by calling `Yarn.Markup.LineParser.ExpandSubstitutions`. + - Built in and Unity Loc line providers now handle this for you. +- `MarkupPallete` now supports more than just colour. +- Dialogue Runner's "Start Automatically" option now defaults to off, not on. +- Dialogue Runner's "On Command" event has been renamed to "On Unhandled Command", to better reflect when it's called. +- `DialogueViewBase` is now deprecated. New Dialogue Views should subclass `AsyncDialogueView`. + - The `DialogueViewBase` class now acts as a compatibility layer for the new + async dialogue view system, and should not be used in new code. +- If a Line Provider fails to return a valid line, the Dialogue Runner will send the Dialogue Views an 'Invalid Line' line, rather than skipping over it completely. +- `InMemoryVariableStorage`'s `debugTextView` property is now a TextMeshPro text field, rather than a legacy Text field. + +### Removed + +- Remove certain items that were previously marked as obsolete: + - Obsolete method `DialogueRunner.ResetDialogue` + - Obsolete property `YarnFunctionAttribute.FunctionName` + - Obsolete property `YarnCommandAttribute.CommandString` + - Obsolete method `YarnProject.GetProgram` +- `ViewBehaviour` enum inside of `AsyncDialogueViewBase`. + +## [2.5.1] 2024-12-17 + +No changes between this version and v2.5.0; this release fixes an issue related to deployment on OpenUPM. + +## [2.5.0] 2024-12-16 + +### Added +- `DialogueRunner.AddCommandHandler` and `DialogueRunner.AddFunction` now validate that the provided names contain no spaces. +- `DialogueRunner.AddCommandHandler` now supports methods whose last parameter is an array of strings. + - This allows for commands with a variable list of parameters. For example, consider the following method: + ```csharp + void LogStrings(int a, string[] remainder) { + Debug.Log($"a = {a}, remainder={string.Join(",", remainder)}"); + } + ``` + This method can be registered as a Yarn command: + ```csharp + dialogueRunner.AddCommandHandler("my_command", LogStrings); + ``` + And called from Yarn Spinner: + ``` + // logs "a = 42, remainder=this,is,pretty,great" + <> + ``` + > [!NOTE] + > Array parameters are required to be string arrays, and are required to be the last parameter of the method. +- Language popups now allow choosing a custom language code. +- Yarn Spinner's XML documentation is now included in the distribution. + +### Changed + +- Fixed an issue where, on Windows, projects would fail to automatically update when a file that belonged to them was created or edited. +- Fixed an issue where Unity Localization `rid` values would change on reimport when they didn't have to. +- Fixed an issue where a `[pause/]` marker at the start of the line would cause all pauses to not work ([@iatenothingbutriceforthreedays](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/pull/291)) +- Inspector-exposed fields on `LineView` are now public. +- Fixed an issue where a `.meta` file was causing warnings to appear in Unity on import. ([@Colbydude](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/pull/294)) +- `YarnProjectImporter.GenerateStringsTable` is now public. +- Yarn Projects now allow choosing more specific cultures (for example 'pt-BR' and 'en-AU' rather than simply 'pt' and 'en') as their base language. +- Fixed a issue where `AudioLineProvider` would throw an exception if an asset was already loaded and was requested a second time. + +## [2.4.2] 2024-02-24 + +### Added + +- Added a Unity Project scoped settings that allows you to override some of the default behaviours of Yarn Spinner. + - Yarn Spinner settings are saved to the path `ProjectSettings\Packages\dev.yarnspinner\YarnSpinnerProjectSettings.json`. + - The settings be changed in the Project Settings window, by choosing `Edit -> Project Settings -> Yarn Spinner`. + - The setting currently supports three convenience features of Yarn Spinner: + - Automatically associating assets with localisations + - Automatically linking `YarnCommand` and `YarnFunction` attributed methods to the Dialogue Runner. + - Generating a `.ysls.json` file that stores information about your Yarn attributed methods. + - This file is saved to `ProjectSettings\Packages\dev.yarnspinner\generated.ysls.json`. + - This is an experimental feature to support better editor integration down the line. As such, this feature defaults to 'off'. + - Enabling or disabling `YarnCommand` and `YarnFunction` linking, or `.ysls` generation, will cause your project to recompile. + - Enabling or disabling asset linking will cause a reimport of all `yarnproject` assets. +- `Yarn.Unity.ActionAnalyser.Action` now has a `MethodIdentifierName` property, which is the short form of the method name. +- `DialogueAdvanceInput` now supports Virtual Button names in addition to KeyCodes and Input Actions. + - This can be configured to work on button or key release or press. By default, the component works on release. +- `LineView` now will add in line breaks when it encounters a self closing `[br /]` marker. +- Yarn attributed Functions and Commands can now use constant values in addition to literals for their name. + +### Changed + +- Update the minimum Unity version to 2021.3. +- Fixed a bug where line pauses could sometimes not happen when the user's framerate is low. +- Fixed a bug where the Rounded Views sample wouldn't import correctly. +- Fixed Minimal Dialogue Runner sample that was using obsolete methods. +- Fixed a bug where TMPShim wasn't being detected. +- Standard library functions (like `random`, `dice`, `round_places`, etc) have been moved to the core Yarn Spinner library. +- Fixed a bug where the audio assets in the samples weren't being linked correctly resulting in playback errors. +- Intro Sample: Moved the Character Color view to a new stand-alone object (it's easier to explain how to do this in a tutorial!) +- `Analyser` no longer ignores non-public methods. + - This is now handled during codegen side so we can better log it. +- `ActionsGenerator` will now generate C# warnings for non-private methods that are attributed as `YarnFunction` or `YarnCommand`. +- `ActionsGenerator` still logs to a temporary location, but now into a `dev.yarnspinner.logs` folder inside the temporary location. +- Auto-advancing `LineView`s will no longer attempt to advance dialogue that has been stopped. +- Actions Registration now dumps generated code into the same temporary folder the logs live in +- `ActionsGenerator` will now generate C# warnings for incorrectly named methods that are attributed as `YarnFunction` or `YarnCommand`. +- Fixed a bug where `AudioLineProvider` didn't allow runtime changing of the text locale. +- Fixed a bug where the Unity Localisation strings tables would have duplicate lines after tagging all lines in a project. + +### Removed + +- Remove certain items that were previously marked as obsolete: + - Obsolete method `DialogueRunner.ResetDialogue` + - Obsolete property `YarnFunctionAttribute.FunctionName` + - Obsolete property `YarnCommandAttribute.CommandString` + - Obsolete method `YarnProject.GetProgram` +- Removed `YarnParameterAttribute` and `YarnStateInjectorAttribute`. + - These attributes were formerly used in earlier versions of Yarn Spinner's + action system, but are no longer used. + +## [2.4.1] 2024-01-30 + +- Version 2.4.1 is the first release of the paid version of Yarn Spinner on the [Unity Asset Store](https://assetstore.unity.com/packages/tools/behavior-ai/yarn-spinner-for-unity-267061) and on [itch.io](https://yarnspinner.itch.io). It's identical to v2.4.0. +- Yarn Spinner is and will remain free and open source - we also make it available for purchase as an excellent way to support the team. +- While you're reading, why not consider our [paid add-ons](https://yarnspinner.itch.io), which add some fantastic and easy-to-customise dialogue views? + +## [2.4.0] 2023-11-15 + +### Added + +#### In-Line Pause Support + +- The built-in Line View now can now identify markup based pauses and insert pauses into the typewriter effect. + - To use this you can use the `pause` markup inside your lines: + ``` + Alice: wow this line now has a halt [pause=500 /] inside of it + ``` + - This line will stop the typewriter for 500ms after the `halt` is shown. After the 500ms delay, the rest of the line will appear. + - Two new Unity events have also been added to be informed when pauses happen: + - `onPauseStarted` + - `onPauseEnded` +- Added a new `PausableTypewriter` effect that works identically to the existing `Typewriter` effect, but supports arbitrary pauses. This effect can be used in your own custom line views to add support for the `[pause/]` markup. +- To learn more about how the pause system works, take a look at the `PausableTypewriter.GetPauseDurationsInsideLine` method! + +#### New Samples + +- Several new sample projects have been added: + - **Shot Reverse Shot** shows how you can use Cinemachine virtual cameras and custom dialogue views to make a shot-reverse-shot scene in your game. + - **Sliced Views** shows off a new alternative default line and option view prefab. + - **Markup Palette** demonstrates the new `MarkupPalette` system. + - **User Input and Yarn** shows how you can use blocking commands and TMP Input fields to get input into Yarn variables. + - **Pausing the Typewriter** howing how you can use the `[pause/]` marker to temporarily pause in the middle of a line. + +#### New Saving Features + +- Added two new basic save methods on `DialogueRunner` that use the persistent data storage location as their save location: + - `SaveStateToPersistentStorage` saves all variables known to the Dialogue Runner to a named file in the [Application.persistentDataPath](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html) folder. + - These methods, `SaveStateToPersistentStorage` and `LoadStateFromPersistentStorage` are intended to replace the older `PlayerPref` based system for basic saves. + - Note: For more complex games, we are still assuming you will need to craft your own bespoke save system. + +#### Markup Palette System + +- The Line View and Options List View prefabs now support _markup palettes_, which allow you to customise the colours of your lines and options using Yarn Spinner's markup. +- Markup Palettes let you associate a marker with a colour. When you use that marker in your dialogue, the text will be rendered using that colour. +- For example, consider the following line: + ``` + I'm [excited]thrilled to be here[/excited]! + ``` + If you create a Markup Palette that links the word `excited` to the colour red, the words "thrilled to be here" will be red. + +- The built-in Line View and Options List View prefabs have support for Markup Palettes, as well as any custom Dialogue Views you build that use the `LineView` and `OptionsListView` classes. + +#### Other Features + +- A new method (`ClearLoadedAssets`), which unloads all cached assets from the `UnityLocalisedLineProvider`, has been added. + - This method will forcibly unload all assets. Only call this method if you're very familiar with the Addressable Assets system and are comfortable with Yarn Spinner's internals! +- Projects can now provide a list of line IDs within a node, using `GetLineIDsForNodes`. + - This is intended to be used to precache multiple nodes worth of assets, but might also be useful for debugging during development. +- Newly created `.yarnproject` files now ignore any `.yarn` files that are in a folder whose name ends in '~', which follows Unity's behaviour. + - You can customise this behaviour by opening the `.yarnproject` file in a text editor and modifying the `excludeFiles` property. +- Added `MarkupPalette` scriptable object and support for the palette inside of `LineView` and `OptionsListView` and associated `OptionView`. + - This is useful both as a standalone way to easily annotate your dialogue, but also as an example of the markup system. + + +### Changed + +- Fixed a bug where `YarnNode` attributes would not display correctly in the Inspector when its property path is longer than 1. +- Fixed a bug in the action registration source code generator that caused it to crash on certain files, which resulted in some commands not being registered at runtime. +- Replaced the call to `Yarn.Compiler.Utility.AddTagsToLines` with `Yarn.Compiler.Utility.TagLines`. +- Fixed incorrect order of generic parameter names for `AddFunction` methods. The usage of these functions is unchanged. +- Fixed incorrect handling of line IDs inside the Unity Localised Line Provider preventing voice assets being loaded. +- Fixed a crash where declaration statements without a value (`<>`) would crash the importer, leading to _weird_ bugs. +- Yarn Functions and Commands can now have up to 10 parameters if you need them. (Previously, the limit was 6.) +- The hard dependency on Text Mesh Pro is now a soft one. + - This change will only affect projects that do not have TextMeshPro installed in their project. For most projects, this change won't be noticed. +- Dialogue Runner will now better wait for line providers to be ready before requesting lines. + - This does have the potential issue of long load times for some larger nodes, in those cases we suggest you preload more lines using `GetLineIDsForNodes` on `YarnProject` +- `UnityLocalisedLineProvider` can now have its default setting of removing unused assets disabled, this is useful when caching multiple nodes worth of assets +- The "Add Assets to Asset Table Collection" wizard now correctly prepends `line:` to the key, to match the documented behaviour. +- `OptionsListView` now deactivates child options when they are not needed instead of just making them transparent. +- When using Unity Localization, line metadata is now stored on the shared entry for a line ID, rather than only on the base language's entry. (This caused an issue where, if the game was not running in the base language, line metadata would not be available.) +- Fixed an issue with `AudioLineProvider` that would prevent audio assets being loaded +- Fixed an issue with the Project editor that prevented audio assets loading when using Addressables. +- The Yarn Project inspector window will now log errors when your inspector width is considered too small. + - We are pretty sure this is a bug in the UI code on Unity's end. + - In our testing it happens at widths less than 319 pixels, because, sure, why not! + - It also doesn't seem to happen in every version of Unity, so that's fun. +- Setting a project on the dialogue runner will now also load the initial variables from this project, fixing this regression. +- `LineView` now supports showing the character names as a standalone element. + - The existing behaviour is still the same with the default prefabs +- `OptionsListView` now supports showing the character names as a standalone element. +- `LineView` now uses the `PausableTypewriter` by default. + - If you don't use pauses, you won't need to change anything. +- `Effects.Typewriter` now is a wrapper into the `PausableTypewriter` effect + - If you don't use pauses nothing will change +- Yarn Projects that have no import data will no longer suggest to upgrade the project file. + - This solves an uncommon but *very* hard to debug error! +- `YarnProjectImporterEditor.CreateUpgradeUI` is now private. +- The Yarn Project editor 'upgrade' help link now correctly links to the upgrade page on the docs. + +### Removed + +- Deprecated `SaveStateToPlayerPrefs` and `LoadStateFromPlayerPrefs`. + - Please use `SaveStateToPersistentStorage` and `LoadStateFromPersistentStorage` instead. +- The Actions class will no longer log every single time a command is registered. +- Removed `YarnLinesAsCanvasText` class and associated elements, this didn't do anything and was using an approach that is no longer advisable. + - The `MainMenu` sample is now gone, this code was not in the package and didn't work, so it is unlikely anyone will notice this has been removed. +- Removed the deprecated code inside `YarnProjectImporterEditor`. +- The Addressable sample has been removed for now as it isn't well suited as an example of using Yarn Spinner and Addressables. It will return in a future release of Yarn Spinner. + +## [2.3.1] 2023-07-07 + +### Added + +- Added the ability to add, modify, and delete source file patterns for Yarn projects in the Inspector. + +### Changed + +- Calling `Stop` on the Dialogue Runner will now also dismiss the LineView, OptionListView, and VoiceOverView. + +## [2.3.0-beta2] 2023-05-17 + +### Added + +- The `YarnScriptTemplate.txt` now has a newline at the end of the file. +- The `.yarnproject` importer has been updated to use new JSON-formatted Yarn Projects. + - JSON-formatted Yarn Projects replace the previous format, which stored all import data in Unity's `.meta` files. + - JSON-formatted Yarn Projects allow a single Yarn script to be used in multiple Yarn Projects, and also allow the Yarn Spinner compiler to support upcoming new features. + - Yarn scripts no longer need to be manually associated with a Yarn Project. If they are in the same folder, or a subfolder, of the Yarn Project, they will be included. + - When you update to 2.3.0-beta2, all Yarn scripts and Yarn Projects will be re-imported. + - **You will receive import errors on your existing Yarn Projects**, telling you that your Yarn Project needs to be upgraded. To do this, follow the message's instructions: select the project, and click Upgrade Yarn Project in the Inspector. + - After upgrading your projects, **you will need to set up your localisations again** by adding them in the Yarn Project's inspector and dragging in your strings file and asset file. + - If your project uses any Yarn files that are not in the same folder, or subfolder, of the Yarn Project, **you will need to move the Yarn files** to the folder. + - Your Yarn scripts, strings files, and localised assets will not be modified, and you won't need to change any objects in your scenes or prefabs. The only thing that will be changed is the Yarn Project file. + +- The Unity Localization integration is now available when the Localization package is installed. + - Prior to this change, the `YARN_USE_EXPERIMENTAL_FEATURES` scripting definition symbol needed to be added to the Player settings. +- You can now add a Dialogue System prefab to your scene by opening the GameObject menu and choosing Yarn Spinner -> Dialogue Runner. +- Added 'On Dialogue Start' event to Dialogue Runner. This event is called when the dialogue starts running. (@nrvllrgrs) + +### Changed +- TextMeshPro dependency is now optional. Example dialogue views will not function properly without TMP. As of 2023.2.0a14 TMP has been merged into UGUI. + +- Added code to invalidate the Program cache on awake for Yarn Projects properly. This means your Yarn Projects will be correctly compiled and referenced in the editor. +- Dialogue Runner will now report an error and stop early if you tell it to start running a node that isn't in the provided Yarn Project. +- Dialogue Runner's 'On Dialogue Complete' event will now fire when you stop it via by calling the `Stop()` method. + +### Removed + + +## [2.3.0-beta1] 2023-03-06 + +### Added + +- Methods tagged with the `YarnCommand` and `YarnFunction` attribute are now discovered at compile time, rather than at run-time. This makes game start-up significantly faster. + - Yarn Spinner for Unity will search your source code for methods with the `YarnCommand` and `YarnFunction` attributes, and generate source code that registers these methods when the game starts up, or when you enter Play Mode in the editor. + + This is a change from previous versions of Yarn Spinner for Unity, which searched for commands and functions at run-time, which had performance and compatibility implications on certain platforms (notably, consoles). + + This search is done automatically in Unity 2021.2 and later. In earlier versions of Unity, you will need to manually tell Yarn Spinner for Unity to check your code, by opening the Window menu and choosing Yarn Spinner -> \mands. +- In Unity 2021.2 and later, you can now see which commands have been registered using `YarnCommand` by opening the Window menu and choosing Yarn Spinner -> Commands... +- `DialogueReference` objects can now be implicitly converted to `string`s. +- The `YarnNode` attribute can be attached to a `string` property to turn it into a drop-down menu for choosing nodes in a Yarn Project. + ```csharp + // A reference to a Yarn Project + public YarnProject project; + + // A node in 'project' + [YarnNode(nameof(project))] + public string node1; + + // Another node in 'project' + [YarnNode(nameof(project))] + public string node2; + ``` + +- The `YarnProject.GetHeaders` method has been added, which fetches all headers for a node. +- The `YarnProject.InitialValues` property has been added, which fetches a dictionary containing the initial values for every variable in the project. + +### Changed + +- Fixed a compile error in the Minimal Viable Dialogue System sample in Unity 2019. + +## [2.2.4] 2023-01-27 + +### Changed + +- Number pluralisation rules have been updated. The rules have now use CLDR version 42.0 (previously, 36.1) +- Fixed an issue where pluralisation markup (i.e. the `plural` and `ordinal` tags) would not work correctly with country-specific locales (for example "en-AU"). + +## [2.2.3] 2022-11-14 + +### Changed + +- Dependency DLLs are now aliased to prevent compilation errors with Burst. + - In v2.2.2, Yarn Spinner's dependency DLLs were renamed to have the prefix `Yarn.` to prevent errors when two DLLs of the same name (e.g. `Google.Protobuf.dll`) are present in the same project. + - This fix solved the edit-time problem, but introduced a new error when the project used Unity's Burst compiler, which looks for DLL files based on their assembly name. + - When compiling with Burst, Unity looks for the DLL file based on the name of the assembly, so when it goes searching for (for example) `Google.Protobuf`, it will _only_ look for the file `Google.Protobuf.dll`, and not the renamed file. + - With this change, the `update_dlls.yml` build script, which pulls in the latest version of Yarn Spinner and its dependencies, now uses the [dotnet-assembly-alias](https://github.com/getsentry/dotnet-assembly-alias/) tool to rename the DLLs _and_ their assembly names. + +## [2.2.2] 2022-10-31 + +### Added + +- The `DialogueReference` class, which stores a reference to a named node in a given project (and shows a warning in the Inspector if the reference can't be found) has been added. Thanks to [@sttz](https://github.com/sttz) for the [contribution](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/pull/189)! +- Initial work on support for the Unity Localization system has been added. + - These features are currently behind a feature flag. They are not yet considered ready for production use, and we aren't offering support for it yet. + - To access them, add the scripting define symbol `YARN_ENABLE_EXPERIMENTAL_FEATURES`. You should only do this if you know what this involves. + - Yarn Project importer now has initial support for Unity's Localization system. + - A new localised line provider subclass, `UnityLocalisedLineProvider.cs` has been added. + +### Changed + +- Fixed interrupt token handling in `VoiceOverView` that would cause it to permanently stop a Dialogue Runner's ability to progress through dialogue lines. +- Fixed an issue where lines and options that contain invalid markup would cause an exception to be thrown, breaking dialogue. A warning message is now logged instead, and the original text of the line (with any invalid markup present) is delivered. +- Fixed a compiler error that made Yarn Spinner fail to compile on Unity 2020.1. +- The `AddCommandHandler(string name, Delegate handler)` and `AddFunction(string name, Delegate handler)` methods on `DialogueRunner` are now `public`. + - These methods allow you to register a `Delegate` object as a command or function. + > **Note:** + > We recommand that you use the pre-existing `AddCommandHandler` and `AddFunction` methods that take `System.Action` or `System.Func` parameters unless you have a very specific reason for using this, as the pre-existing methods allow the compiler to do type-checking on your command and function implementations. +- Fixed an issue that would cause compilation errors if a Unity project using Yarn Spinner also used a DLL with the same name as one of Yarn Spinner's dependencies (for example Google Protocol Buffers). + - The dependency DLLs that come with Yarn Spinner (for example, `Antlr.Runtime`, `Google.Probuf`, and others) have been renamed to have the prefix `Yarn.`, and the assembly definition files for Yarn Spinner have been updated to use the renamed files. + - Huge thanks to [@Sygan](https://github.com/Sygan) for finding and describing [the fix for this problem](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues/15#issuecomment-1036162152)! +- The `YarnProject.GetProgram()` method has been replaced with a property, `Program`. + - `GetProgram()` still exists, but has been marked as obsolete and will be removed in a future release of Yarn Spinner. + - `YarnSpinner.Program` has better performance, because it caches the result of de-serializing the compiled Yarn Program. + +## [2.2.1] 2022-06-14 + +### Changed + +- Fixed an issue where Yarn projects that made use of the `visited` or `visit_count` functions could produce errors when re-importing the project. + +## [2.2.0] 2022-04-08 + +### Added + +- A simple, built-in system for saving and loading Yarn variables to the built-in PlayerPrefs object has been added. + - Call `DialogueRunner.SaveStateToPlayerPrefs` to save all variables to the `PlayerPrefs` system, and `DialogueRunner.LoadStateFromPlayerPrefs` to load from `PlayerPrefs` into the variable storage. + - These methods do not write to file (except via `PlayerPrefs`, which handles disk writing on its own), and only work with variables (and not information like which line is currently being run.) +- Metadata for each line is exposed through a Yarn Project. Metadata generally comes as hashtags similar to `#line`. They can be used to define line-specific behavior (no particular behavior is supported by default, each user will need to define their own). +- When exporting Strings files, a Yarn Project will also export another CSV file with the line metadata (for each line with metadata). +- `LocalizedLine`s now contain a field for any metadata associated with the line. +- `YarnFunction` tagged methods now appear in the inspector window for the project, letting you view their basic info. + +### Changed + +- `YarnPreventPlayMode` no longer uses `WeakReference` pointing to `Unity.Object` (this is unsupported by Unity). +- `ActionManager` no longer logs every single command that it registers. (#165) +- Line view should no longer have unusual interactions around enabling and disabling different effects (#161 and #153). +- Improved the type inference system around the use of functions. +- Fixed exception when viewing a Yarn Project in the inspector that contains no declarations, in Unity 2021.2 and earlier (#168) + +This has two pieces, the first is in YarnSpinner Core and adds in support for partial backwards type inference. +This means in many situations where either the l-value or r-value of an expression is known that can be used to provide a type to the other side of the equation. +Additionally now functions tagged with the `YarnFunction` attribute are sent along to the compiler so that they can be used to inform values. +The upside of this is in situations like `<>` if either `$cats` is declared or `get_cats` is tagged as a `YarnFunction` there won't be an error anymore. + +### Removed + +- The `SerializeAllVariablesToJSON` and `DeserializeAllVariablesFromJSON` methods have been removed. + - If you need a simple way to save all variables, use `DialogueRunner.SaveStateToPlayerPrefs` and `DialogueRunner.LoadStateFromPlayerPrefs` instead, which save directly to Unity's PlayerPrefs system and don't require reading or writing files. + - If your saving or loading needs are more complex, use the `VariableStorageBehaviour` class's `GetAllVariables()` and `SetAllVariables()` methods to get and set the value of all values at once, and handle the serialization and deserialization the way your game needs it. + +## [2.1.0] 2022-02-17 + +### Dialogue View API Update + +The API for creating new Dialogue Views has been updated. + +> **Background:** Dialogue Views are objects that receive lines and options from the Dialogue Runner, present them to the player, receive option selections, and signal when the user should see the next piece of content. They're subclasses of the [`DialogueViewBase`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueviewbase) class. All of our built-in Dialogue Views, such as [`LineView`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.lineview) and [`OptionsListView`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.optionslistview), are examples of this. + +Previously, line objects stored information about their current state, and as Dialogue Views reported that they had finished presenting or dismissing their line, all views would receive a signal that the line's state had changed, and respond to those changes by changing the way that the line was presented (such as by dismissing it when the line's state became 'Dismissed'). This meant that every Dialogue View class needed to implement fairly complex logic to handle these changes in state. + +In this release, 'line state' is no longer a concept that Dialogue Views need to keep track of. Instead, Dialogue Views that present lines simply need to implement three methods: + +- [`RunLine`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueviewbase/yarn.unity.dialogueviewbase.runline) is called when the Dialogue Runner wants to show a line to the player. It receives a line, as well as a completion handler to call when the line view has finished delivering the contents line. +- [`InterruptLine`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueviewbase/yarn.unity.dialogueviewbase.interruptline) is called when the Dialogue Runner wants all Dialogue Views to finish presenting their lines as fast as possible. It receives the line that's currently being presented, as well as a new completion handler to call when the presentation is finished (which this method should try and call as quickly as it can.) +- [`DismissLine`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueviewbase/yarn.unity.dialogueviewbase.dismissline) is called when all Dialogue Views have finished delivering their line (whether it was interrupted, or whether it completed normally). It receives a completion handler to call when the dismissal is complete. + +The updated flow is this: + +1. While running a Yarn script, the Dialogue Runner encounters a line of dialogue to show to the user. +2. It calls [`RunLine`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueviewbase/yarn.unity.dialogueviewbase.runline) on all Dialogue Views, and waits for all of them to call their completion handler to indicate that they're done presenting the line. + * At any point, a Dialogue View can call a method that requests that the current line be interrupted. When this happens, the Dialogue Runner calls [`InterruptLine`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueviewbase/yarn.unity.dialogueviewbase.interruptline) on the Dialogue Views, and waits for them to call the new completion handler to indicate that they've finished presenting the line. +3. Once all Dialogue Views have reported that they're done, the Dialogue Runner calls [`DismissLine`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueviewbase/yarn.unity.dialogueviewbase.dismissline) on all Dialogue Views, and waits for them to call the completion handler to indicate that they're done dismissing the line. +4. The Dialogue Runner then moves on to the next piece of content. + +This new flow significantly simplifies the amount of information that a Dialogue View needs to keep track of, as well as the amount of logic that a Dialogue View needs to have to manage this information. + +Instead, it's a simpler model of: "when you're told to run a line, run it, and tell us when you're done. When you're told to interrupt the line, finish running it ASAP and tell us when you're done. Finally, when you're told to dismiss your line, do it and let us know when you're done." + +We've also moved the user input handling code out of the built-in [`LineView`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.lineview) class, and into a new class called [`DialogueAdvanceInput`](https://docs.yarnspinner.dev/api/csharp/yarn.unity/yarn.unity.dialogueadvanceinput). This class lets you use either of the Unity Input systems to signal to a DialogueView that the user wants to advance the dialogue; by moving it out of our built-in view, it's a little easier for Dialogue View writers, who may not want to have to deal with input. + +This hopefully alleviates some of the pain points in issues relating to how Dialogue Views work, like [issue #95](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues/95). + +There are no changes to how options are handled in this new API. + +### Jump to Expressions + +- The `<>` statement can now take an expression. + +```yarn +<> +<> +``` + +- Previously, the `jump` statement required the name of a node. With this change, it can now also take an expression that resolves to the name of a node. +- Jump expressions may be a constant string, a variable, a function call, or any other type of expression. +- These expressions must be wrapped in curly braces (`{` `}`), and must produce a string. + +### Added + +- Added a new component, `DialogueAdvanceInput`, which responds to user input and tells a Dialogue View to advance. +- Added `DialogueViewBase.RunLine` method. +- Added `DialogueViewBase.InterruptLine` method. +- Added `DialogueViewBase.DismissLine` method. + +### Changed + +- Updated to Yarn Spinner Core 2.1.0. +- Updated `DialogueRunner` to support a new, simpler API for Dialogue Views; lines no longer have a state for the Dialogue Views to respond to changes to, and instead only receive method calls that tell them what to do next. +- Fixed a bug where changing a Yarn script asset in during Play Mode would cause the string table to become empty until the next import (#154) +- Fixed a bug where Yarn functions outside the default assembly would fail to be loaded. + +### Removed + +- Removed `LineStatus` enum. +- Removed `DialogueViewBase.OnLineStatusChanged` method. +- Removed `DialogueViewBase.ReadyForNextLine` method. +- Removed `VoiceOverPlaybackFmod` class. (This view was largely unmaintained, and we feel that it's best for end users to customise their FMOD integration to suit their own needs.) +- Renamed `VoiceOverPlaybackUnity` to `VoiceOverView`. + +## [2.0.2] 2022-01-08 + +### Added + +- You can now specify which assemblies you want Yarn Spinner to search for `YarnCommand` and `YarnFunction` methods in. + - By default, Yarn Spinner will search in your game's code, as well as every assembly definition in your code and your packages. + - You can choose to make Yarn Spinner only look in specific assembly definitions, which reduces the amount of time needed to search for commands and functions. + - To control how Yarn Spinner searches for commands and actions, turn off "Search All Assemblies" in the Inspector for a Yarn Project. +- Added a Spanish translation to the Intro sample. + +### Changed + +- ActionManager now only searches for commands and actions in assemblies that Yarn Projects specify. This significantly reduces startup time and memory usage. +- Improved error messages when calling methods defined via the `YarnCommand` attribute where the specified object can't be found. + +## [2.0.1] 2021-12-23 + +### Added + +- The v1 to v2 language upgrader now renames node names that have a period (`.`) in their names to use underscores (`_`) instead. Jumps and options are also updated to use these new names. + +### Changed + +- Fixed a crash in the compiler when producing an error message about an undeclared function. +- Fixed an error when a constant float value (such as in a `<>` statement) was parsed and the user's current locale doesn't use a period (`.`) as the decimal separator. + +## [2.0.0-rc1] 2021-12-13 + +### Added + +- Command parameters can now be grouped with double quotes. eg. `<` and `<>` (@andiCR) + +- You can now add dialogue views to dialogue runner at any time. + +- The inspector for Yarn scripts now allows you to change the Project that the script belongs to. (@radiatoryang) + +- Yarn script compile errors will prevent play mode. + +- Default functions have been added for convenience. + - `float random()` - returns a number between 0 and 1, inclusive (proxies Unity's default prng) + - `float random_range(float, float)` - returns a number in a given range, inclusive (proxies Unity's default prng) + - `int dice(int)` - returns an integer in a given range, like a dice (proxies Unity's default prng) + - For example, `dice(6) + dice(6)` to simulate two dice, or `dice(20)` for a D20 roll. + - `int round(float)` - rounds a number using away-from-zero rounding + - `float round_places(float, int)` - rounds a number to n digits using away-from-zero rounding + - `int floor(float)` - floors a number (towards negative infinity) + - `int ceil(float)` - ceilings a number (towards positive infinity) + - `int int(float)` - truncates the number (towards zero) + - `int inc(float | int)` - increments to the next integer + - `int dec(float | int)` - decrements to the previous integer + - `int decimal(float)` - gets the decimal portion of the float + +- The `YarnFunction` attribute has been added. + - Simply add it to a static function, eg + + ```cs + [YarnFunction] // registers function under "example" + public static int example(int param) { + return param + 1; + } + + [YarnFunction("custom_name")] // registers function under "custom_name" + public static int example2(int param) { + return param * param; + } + ``` + +- The `YarnCommand` attribute has been improved and made more robust for most use cases. + - You can now leave the name blank to use the method name as the registration name. + + ```cs + [YarnCommand] // like in previous example with YarnFunction. + void example(int steps) { + for (int i = 0; i < steps; i++) { ... } + } + + [YarnCommand("custom_name")] // you can still provide a custom name if you want + void example2(int steps) { + for (int i = steps - 1; i >= 0; i--) { ... } + } + ``` + + - It now recognizes static functions and does not attempt to use the first parameter as an instance variable anymore. + + ```cs + [YarnCommand] // use like so: <> + static void example() => ...; + + [YarnCommand] // still as before: <> + void example2() => ...; + ``` + + - You can also define custom getters for better performance. + + ```cs + [YarnStateInjector(nameof(GetBehavior))] // if this is null, the previous behavior of using GameObject.Find will still be available + class CustomBehavior : MonoBehaviour { + static CustomBehavior GetBehavior(string name) { + // e.g., it may only exist under a certain transform, or you have a custom cache... + // or it's built from a ScriptableObject... + return ...; + } + + [YarnCommand] // the "this" will be as returned from GetBehavior + void example() => Debug.Log(this); + + // special variation on getting behavior + static CustomBehavior GetBehaviorSpecial(string name) => ...; + + [YarnCommand(Injector = nameof(GetBehaviorSpecial))] + void example_special() => Debug.Log(this); + } + ``` + + - You can also define custom getters for Component parameters in the same vein. + + ```cs + class CustomBehavior : MonoBehaviour { + static Animator GetAnimator(string name) => ...; + + [YarnCommand] + void example([YarnParameter(nameof(GetAnimator))] Animator animator) => Debug.Log(animator); + } + ``` + + - You should continue to use manual registration if you want to make an instance function (ie where the ["target"](https://docs.microsoft.com/en-us/dotnet/api/system.delegate.target?view=netstandard-2.0) is defined) static. + +- Sample scenes now have a render pipeline detector gameobject that will warn when the sample scene materials won't look correct in the current render pipeline. + +- Variables declared inside Yarn scripts will now have the default values set into the variable storage. + +### Changed + +- Updated to support new error handling in Yarn Spinner. + - Yarn Spinner longer reports errors by throwing an exception, and instead provides a collection of diagnostic messages in the compiler result. In Unity, Yarn Spinner will now show _all_ error messages that the compiler may produce. + +- The console will no longer report an error indicating that a command is "already defined" when a subclass of a MonoBehaviour that has `YarnCommand` methods exists. + +- `LocalizedLine.Text`'s setter is now public, not internal. + +- `DialogueRunner` will now throw an exception if a dialogue view attempts to select an + option on the same frame that options are run. + +- `DialogueRunner.VariableStorage` can now be modified at runtime. + +- Calling `DialogueRunner.StartDialogue` when the dialogue runner is already running will now result in an error being logged. + +- Line Views will now only enable and disable action references if the line view is also configured to use said action. + +- Yarn Project importer will now save variable declaration metadata on the first time + +### Removed + +- Support for Unity 2018 LTS has been dropped, and 2019 LTS (currently 2019.4.32f1) will be the minimum supported version. The support scheme for Yarn Spinner will be clarified in the [CONTRIBUTING](./CONTRIBUTING.md) docs. If you still require support for 2018, please join our [Discord](https://discord.gg/yarnspinner)! + +## [2.0.0-beta5] 2021-08-17 + +### Added + +* `InMemoryVariableStorage` now throws an exception if you attempt to get or set a variable whose name doesn't start with `$`. +* `LineView` now has an onCharacterTyped event that triggers for each character typed in the typewriter effect. + +### Changed + +* `OptionsListView` no longer throws a `NullPointerException` when the dialogue starts with options (instead of a line.) +* When creating a new Yarn Project file from the Assets -> Create menu, the correct icon is now used. +* Updated to use the new type system in Yarn Spinner 2.0-beta5. + +### Removed + +* Yarn Programs: The 'Convert Implicit Declarations' button has been temporarily removed, due to a required compatibility change related to the new type system. It will be restored before final 2.0 release. + +## [2.0.0-beta4] 2021-04-01 + +Yarn Spinner 2.0 Beta 4 is a hotfix release for Yarn Spinner 2.0 Beta 3. + +### Changed + +- Fixed an issue that caused Yarn Spinner to not compile on Unity 2018 or Unity 2019. + +## [2.0.0-beta3] 2021-03-27 + +### Added + +- The dialogue runner can now be configured to log less to the console, reducing the amount of noise it generates there. (@radiatoryang) +- Warning messages and errors now appear to help users diagnose two common problems: (1) not adding a Command properly, (2) can't find a localization entry for a line (either because of broken line tag or bad connection to Localization Database) (@radiatoryang) +- Made options that have a line condition able to be presented to the player, but made unavailable. +- This change was made in order to allow games to conditionally present, but disallow, options that the player can't choose. For example, consider the following script: + +``` +TD-110: Let me see your identification. +-> Of course... um totally not General Kenobi and the son of Darth Vader. + Luke: Wait, what?! + TD-110: Promotion Time! +-> You don't need to see his identification. <> + TD-110: We don't need to see his identification. +``` + +- If the variable `$learnt_mind_trick` is false, a game may want to show the option but not allow the player to select it (i.e., show that this option could have been chosen if they'd learned how to do a mind trick.) +- In previous versions of Yarn Spinner, if a line condition failed, the entire option was not delivered to the game. With this change, all options are delivered, and the `OptionSet.Option.IsAvailable` variable contains `false` if the condition was not met, and `true` if it was (or was not present.) +- The `DialogueUI` component now has a "showUnavailableOptions" option that controls the display behaviour of unavailable options. If it's true, then unavailable options are presented, but not selectable; if it's false, then unavailable options are not presented at all (i.e. same as Yarn Spinner 1.0.) +- Audio for lines in a `Localization` object can now be previewed in the editor. (@radiatoryang) +- Lines can be added to a `Localization` object at runtime. They're only stored in memory, and are discarded when gameplay ends. +- Commands that take a boolean parameter now support specifying that parameter by its name, rather than requiring the string `true`. +- For example, if you have a command like this: + +```csharp + [YarnCommand("walk")] + void WalkToPoint(string destinationName, bool wait = false) { + // ... + } +``` + +Previously, you'd need to use this in your Yarn scripts: + +``` +<> +``` + +With this change, you can instead say this: + +``` +<> +``` + +- New icons for Yarn Spinner assets have been added. +- New dialogue views, `LineView` and `OptionListView`, have been added. These are intended to replace the previous `DialogueUI`, make use of TextMeshPro for text display, and allow for easier customisation through prefabs. +- `DialogueRunner`s will now automatically create and use an `InMemoryVariableStorage` object if one isn't provided. +- The Inspector for `DialogueRunner` has been updated, and is now easier to use. + +### Changed + +- Certain private methods in `DialogueUI` have changed to protected, making it easier to subclass (@radiatoryang) +- Fixed an issue where option buttons from previous option prompts could re-appear in later prompts (@radiatoryang) +- Fixed an issue where dialogue views that are not enabled were still being waited for (@radiatoryang) +- Upgrader tool now creates new files on disk, where needed (for example, .yarnproject files) +- `YarnProgram`, the asset that stores references to individual .yarn files for compilation, has been renamed to `YarnProject`. Because this change makes Unity forget any existing references to "YarnProgram" assets, **when upgrading to this version, you must set the Yarn Project field in your Dialogue Runners again.** +- `Localization`, the asset that mapped line IDs to localized data, is now automatically generated for you by the `YarnProject`. + - You don't create them yourselves, and you no longer need to manually refresh them. + - The `YarnProject` always creates at least one localization: the "Base" localization, which contains the original text found in your `.yarn` files. + - You can create more localizations in the `YarnProject`'s inspector, and supply the language code to use and a `.csv` file containing replacement strings. +- Renamed the 'StartHere' demo to 'Intro', because it's not actually the first step in installing Yarn Spinner. +- Simplified the workflow for working with Addressable Assets. + - You now import the package, enable its use on your Yarn Project, and click the Update Asset Addresses button to ensure that all assets have an address that Yarn Spinner knows about. +- The 3D, VisualNovel, and Intro examples have been updated to use the new `LineView` and `OptionsListView` components, rather than `DialogueUI`. +- `DialogueRunner.ResetDialogue` is now marked as Obsolete (it had the same effect as just calling `StartDialogue` anyway.) +- The `LineStatus` enum's values have been renamed, to better convey their purpose: + - `Running` is now `Presenting`. + - `Interrupted` remains the same. + - `Delivered` is now `FinishedPresenting`. + - `Ended` is now `Dismissed` . +- The `ResetDialogue()` method now takes an optional parameter to restart from. If none is provided, the dialogue runner attempts to restart from the start node, followed by the current node, or else throws an exception. +- `DialogueViewBase.MarkLineComplete`, the method for signalling that the user wants to interrupt or proceed to the next line, has been renamed to `ReadyForNextLine`. +- `DialogueRunner.continueNextLineOnLineFinished` has been renamed to `automaticallyContinueLines`. + +### Removed + +- `LocalizationDatabase`, the asset that stored references to `Localization` assets and manages per-locale line lookups, has been removed. This functionality is now handled by `YarnProject` assets. You no longer supply a localization database to a `DialogueRunner` or to a `LineProvider` - the work is handled for you. +- `AddressableAudioLineProvider` has been removed. `AudioLineProvider` now works with addressable assets, if the package is installed and your Yarn Project has been configured to use them. +- You no longer specify a list of languages available to your project in the Preferences menu or in the project settings. This is now controlled from the Yarn Project. +- The `StartDialogue()` method (with no parameters) has been removed. Instead, provide a node name to start from when calling `StartDialogue(nodeName)`. + +## [2.0.0-beta2] 2021-01-14 + +### Added + +- InMemoryVariableStorage now shows the current state of variables in the Inspector. (@radiatoryang) +- InMemoryVariableStorage now supports saving variables to file, and to PlayerPrefs. (@radiatoryang) + +### Changed + +- Inline expressions (for example, `One plus one is {1+1}`) are now expanded. +- Added Help URLs to various classes. (@radiatoryang) +- The Upgrader window (Window -> Yarn Spinner -> Upgrade Scripts) now uses the updated + Yarn Spinner upgrade tools. See Yarn Spinner 2.0.0-beta2 release notes for more + information on the upgrader. +- Fixed an issue where programs failed to import if a source script reference is invalid +- Fixed an issue where the DialogueUI would show empty lines when showCharacterName is + false and the line has no character name + +### Removed + +- The `[[Destination]]` and `[[Option|Destination]]` syntax has been removed from the language. + - This syntax was inherited from the original Yarn language, which itself inherited it from Twine. + - We removed it for four reasons: + - it conflated jumps and options, which are very different operations, with too-similar syntax; + - the Option-destination syntax for declaring options involved the management of non-obvious state (that is, if an option statement was inside an `if` branch that was never executed, it was not presented, and the runtime needed to keep track of that); + - it was not obvious that options accumulated and were only presented at the end of the node; + - finally, shortcut options provide a cleaner way to present the same behaviour. + - We have added a `<>` command, which replaces the `[[Destination]]` jump syntax. + - No change to the bytecode is made here; these changes only affect the compiler. + - Instead of using ``[[Option|Destination]]`` syntax, use shortcut options instead. For example: + +``` +// Before +Kim: You want a bagel? +[[Yes, please!|GiveBagel]] +[[No, thanks!|DontWantBagel]] + +// After +Kim: You want a bagel? +-> Yes, please! + <> +-> No, thanks! + <> +``` + +- InMemoryVariableStorage no longer manages 'default' variables (this concept has moved to the Yarn Program.) (@radiatoryang) + +## [2.0.0-beta1] 2020-10-19 + +### Added + +- Added a 3D speech bubble sample, for dynamically positioning a speech bubble above a game character. (@radiatoryang) +- Added a phone chat sample. (@radiatoryang) +- Added a visual novel template. (@radiatoryang) +- Added support for voice overs. (@Schroedingers-Cat) +- Added a new API for presenting and managing lines of dialogue. +- Added a new API for working with localizations. +- Added an option to DialogueUI to allow hiding character names. +- The Yarn Spinner Window (Window -> Yarn Spinner) now shows the current version of Yarn Spinner. + +### Changed + +- Individual `.yarn` scripts are now combined into a single 'Yarn Program', which is what you provide to your `DialogueRunner`. You no longer add multiple `.yarn` files to a DialogueRunner. To create a new Yarn Program, open the Asset menu, and choose Create -> Yarn Spinner -> Yarn Program. You can also create a new Yarn Program by selecting a Yarn Script, and clicking Create New Yarn Program. +- Version 2 of the Yarn language requires variables to be declared. You can declare them in your .yarn scripts, or you can declare them in the Inspector for your Yarn Program. + - Variables must always have a defined type, and aren't allowed to change type. This means, for example, that you can't store a string inside a variable that was declared as a number. + - Variables also have a default value. As a result, variables are never allowed to be `null`. + - Variable declarations can be in any part of a Yarn script. As long as they're somewhere in the file, they'll be used. + - Variable declarations don't have to be in the same file as where they're used. If the Yarn Program contains a script that has a variable declaration, other scripts in that Program can use the variable. + - To declare a variable on a Yarn Program, select it, and click the `+` button to create the new variable. + - To declare a variable in a script, use the following syntax: + +``` +<> // declares a string +<> // declares a number +<> // declares a boolean +``` + +- Nicer error messages when the localized text for a line of dialogue can't be found. +- DialogueUI is now a subclass of DialogueViewBase. +- Moved Yarn Spinner classes into the `Yarn.Unity` namespace, or one of its children, depending on its purpose. +- Dialogue.AddFunction now uses functions that can take multiple parameters. You no longer use a single `Yarn.Value[]` parameter; you can now have up to 5, which can be strings, integers, floats, doubles, bools, or `Yarn.Value`s. +- Commands registered via the `YarnCommand` attribute can now take parameter types besides strings. Parameters can also be optional. Additionally, these methods are now cached, and are faster to call. + +### Removed +- Commands registered via the `YarnCommand` attribute can no longer accept a `params` array of parameters. If your command takes a variable number of parameters, use optional parameters instead. + +## [1.2.7] + +### Changed + +- Backported check for Experimental status of AssetImporter (promoted in 2020.2) + +## [1.2.6] + +Note: Versions 1.2.1 through 1.2.5 are identical to v1.2.0; they were version number bumps while we were diagnosing an issue in OpenUPM. + +### Changed + +- Fixed compiler issues in Unity 2019.3 and later by adding an explicit reference to YarnSpinner.dll in YarnSpinnerTests.asmdef + +## [1.2.0] 2020-05-04 + +This is the first release of v1.2.0 of Yarn Spinner for Unity as a separate project. Previously, this project's source code was part of the Yarn Spinner repository. Version 1.2.0 of Yarn Spinner contains an identical release to this. + +### Added + +- Yarn scripts now appear with Yarn Spinner icon. (@Schroedingers-Cat) +- Documentation is updated to reflect the current version number (also to mention 2018.4 LTS as supported) +- Added a button in the Inspector for `.yarn` files in Yarn Spinner for Unity, which updates localised `.csv` files when the `.yarn` file changes. (@stalhandske, https://github.com/YarnSpinnerTool/YarnSpinner/issues/227) +- Added handlers for when nodes begin executing (in addition to the existing handlers for when nodes complete.) (@arendhil, https://github.com/YarnSpinnerTool/YarnSpinner/issues/222) +- `OptionSet.Option` now includes the name of the node that an option will jump to if selected. +- Added unit tests for Yarn Spinner for Unity (@Schroedingers-Cat) +- Yarn Spinner for Unity: Added a menu item for creating new Yarn scripts (Assets -> Create -> Yarn Script) +- Added Nuget package definitions for [YarnSpinner](http://nuget.org/packages/YarnSpinner/) and [YarnSpinner.Compiler](http://nuget.org/packages/YarnSpinner.Compiler/). + +### Changed + +- Fixed a crash in the compiler when parsing single-character commands (e.g. `<

>`) (https://github.com/YarnSpinnerTool/YarnSpinner/issues/231) +- Parse errors no longer show debugging information in non-debug builds. \ No newline at end of file diff --git a/Packages/dev.yarnspinner.unity/CHANGELOG.md.meta b/Packages/dev.yarnspinner.unity/CHANGELOG.md.meta new file mode 100644 index 00000000..2c8cf992 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: db3ac383f37b340b593e3cd5d16b4cf5 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md b/Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..cfa626ad --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at yarnspinner@secretlab.com.au. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md.meta b/Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md.meta new file mode 100644 index 00000000..575b35b0 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5cfcecef19b8f44c8a2e435d277c5a04 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/CONTRIBUTING.md b/Packages/dev.yarnspinner.unity/CONTRIBUTING.md new file mode 100644 index 00000000..4d8f63ea --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to Yarn Spinner + +Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. + +## How to send in your contributions + +There are many ways you can send your contributions to Yarn Spinner. You can either **report a bug**, or you can make the changes yourself and **submit a pull request**! + +### Reporting bugs and opening issues + +Please [report bugs](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues) and open issues generously. Don't be afraid that your idea is silly, or you're reporting a duplicate. We're happy to hear from you. Seriously. + +> ***Please Note:*** Yarn Spinner is written by volunteers. If you encounter a problem while using it, we'll do our best to help you, but neither the authors, or Secret Lab Pty. Ltd. can offer any support. + +### Submitting a pull request + +* [Fork](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/fork) and clone the repository +* Create a new branch: git checkout -b my-branch-name +* Make your changes +* Push to your fork and [submit a pull request](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/compare) +* Pat your self on the back and wait for your pull request to be reviewed. + +If you're unfamiliar with how pull requests work, [GitHub's documentation on them](https://help.github.com/articles/using-pull-requests/) is very good. + +Here are a few things you can do that will increase the likelihood of your pull request being accepted: + +* Update the documentation as necessary, as well as making code changes. +* Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. +* [Write a good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). + +### Unity & LTS support + +As we are a group of volunteers, we can only officially support the versions of Unity that are actively supported by Unity themselves. Please check the official [LTS FAQ](https://support.unity.com/hc/en-us/articles/4403332003348-What-is-a-Unity-LTS-Long-Term-Support-version-and-what-can-I-expect-from-it-) for more up-to-date information. Currently, the minimum supported version is **2019.4**. + +However, if you would like to submit contributions to fix support for unsupported versions, please go ahead and do so, but do note that we cannot guarantee that it continues to work for that version. + +### Branches + +All of Yarn Spinner's in-progress work happens on the `main` branch. When we make releases, we create a new tag from `main`. Larger features are developed on their own branch, and then merged to `main` when ready. + +### Code and other contributions + +Contributions to Yarn Spinner (via pull request or otherwise) must be licensed under the MIT license. diff --git a/Packages/dev.yarnspinner.unity/CONTRIBUTING.md.meta b/Packages/dev.yarnspinner.unity/CONTRIBUTING.md.meta new file mode 100644 index 00000000..9829ac09 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CONTRIBUTING.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9e079b710417049b2915eacde0b91e1b +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/CONTRIBUTORS.md b/Packages/dev.yarnspinner.unity/CONTRIBUTORS.md new file mode 100644 index 00000000..3eb94d2e --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CONTRIBUTORS.md @@ -0,0 +1,21 @@ +# Contributors + +The following people have contributed to the development of Yarn Spinner. If you submit a pull request, please add your name to the list below. + +* 2015-ongoing: Secret Lab Team - Dr Jon Manning and Dr Paris Buttfield-Addison +* 2017: Rev Peter Lawler +* 2017: Dr Tim 'McJones' Nugent +* 2018: Damon 'demanrisu' Reece +* 2019: Tamme Schichler +* 2020: @Schroedingers-Cat, Robert Yang (https://debacle.us) +* 2021: Jonathan MacAlpine +* 2021: Shane Marks (https://necrosoftgames.com) +* 2021: @andiCR, Andrés Cartín (andres@treeinteractivecr.com) +* 2021: Shane Duan +* 2022: Bernardo Vecchia Stein +* 2023: ChocolaMint (https://chocola-mint.github.io/) +* 2023: Mitch Zais +* 2023: Thomas Ingram (https://vertx.xyz) +* 2023: Isaac Berman (https://github.com/bermanisaac) +* 20??: Mars Buttfield-Addison (https://github.com/TheMartianLife) +* 2025: Jay Xavier Peet (https://github.com/JayPeet) \ No newline at end of file diff --git a/Packages/dev.yarnspinner.unity/CONTRIBUTORS.md.meta b/Packages/dev.yarnspinner.unity/CONTRIBUTORS.md.meta new file mode 100644 index 00000000..aae1ec91 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/CONTRIBUTORS.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9db75e8c06dd1484bb6f0c05ecc00b7f +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor.meta b/Packages/dev.yarnspinner.unity/Editor.meta new file mode 100644 index 00000000..99831ad5 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c3b025f9df1834d5e91c6873ece463b1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis.meta new file mode 100644 index 00000000..f09c4937 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 991bb6a4238c84092a59ac23d54fda95 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs new file mode 100644 index 00000000..18e0b780 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs @@ -0,0 +1,1025 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; + +#nullable enable + +namespace Yarn.Unity.ActionAnalyser +{ + public struct Position + { + public int Line; + public int Column; + } + + public struct Range + { + public Position Start; + public Position End; + + public static implicit operator Range(FileLinePositionSpan span) + { + var start = span.StartLinePosition; + var end = span.EndLinePosition; + + return new Range + { + Start = { + Line = start.Line + 1, + Column = start.Character + 1, + }, + End = { + Line = end.Line + 1, + Column = end.Character + 1, + }, + }; + } + } + + public enum ActionType + { + ///

+ /// The method represents a command. + /// + Command, + /// + /// The method represents a function. + /// + Function, + /// + /// The method may have been intended to be an action, but its type + /// cannot be determined. + /// + Invalid, + /// + /// The method is not a Yarn action. + /// + + NotAnAction, + } + + public enum DeclarationType + { + /// + /// The action is declared via a YarnCommand or YarnFunction attribute. + /// + Attribute, + /// + /// The action is declared by calling AddCommandHandler or AddFunction + /// on a DialogueRunner. + /// + DirectRegistration + } + + public enum AsyncType + { + /// + /// The action operates synchronously. + /// + Sync, + /// + /// The action may operate asynchronously, and Dialogue Runners should + /// check the return value of the action to determine whether to block + /// on the method call or not. + /// + /// + /// This is only valid for objects whose is . + /// + MaybeAsyncCoroutine, + /// + /// The action operates asynchronously using a coroutine. + /// + AsyncCoroutine, + + /// + /// The action operates asynchronously through c# async infrastructure + /// + AsyncTask, + } + + static class ITypeSymbolExtension + { + public static string GetYarnTypeString(this ITypeSymbol typeSymbol) + { + return typeSymbol.SpecialType switch + { + SpecialType.System_Boolean => "bool", + SpecialType.System_SByte => "number", + SpecialType.System_Byte => "number", + SpecialType.System_Int16 => "number", + SpecialType.System_UInt16 => "number", + SpecialType.System_Int32 => "number", + SpecialType.System_UInt32 => "number", + SpecialType.System_Int64 => "number", + SpecialType.System_UInt64 => "number", + SpecialType.System_Decimal => "number", + SpecialType.System_Single => "number", + SpecialType.System_Double => "number", + SpecialType.System_String => "string", + _ => "any" + }; + } + } + + public struct Parameter + { + public bool IsOptional; + public string Name; + public ITypeSymbol Type; + public string? Description; + public string? DefaultValueString; + public bool IsParamsArray; + + public AttributeData[]? Attributes; + + public readonly string YarnTypeString => Type.GetYarnTypeString(); // this should change to support the subtypes through the same logic we use below, for now it's fine + } + + public class Action + { + public Action(string name, ActionType type, IMethodSymbol methodSymbol) + { + Name = name; + Type = type; + MethodSymbol = methodSymbol; + } + + /// + /// The name of this action. + /// + public string Name { get; internal set; } + + /// + /// The method symbol for this action. + /// + public IMethodSymbol MethodSymbol { get; internal set; } + + public string? Description { get; internal set; } + + /// + /// The declaration of this action's method, if available. + /// + public SyntaxNode? Declaration { get; internal set; } + + /// + /// The type of the action. + /// + public ActionType Type { get; internal set; } + + /// + /// The declaration type of the action. + /// + public DeclarationType DeclarationType { get; internal set; } + + /// + /// The sync/async type of the action. + /// + public AsyncType AsyncType { get; internal set; } + + /// + /// The that can be + /// used to answer semantic queries about this method. + /// + internal SemanticModel? SemanticModel { get; set; } + + /// + /// The fully-qualified name for this method, including the global + /// prefix. + /// + public string? MethodName { get; set; } + + /// + /// Gets the short form of the method, essentially the easy to read form of . + /// + public string? MethodIdentifierName { get; internal set; } + + /// + /// Whether this action is a static method, or an instance method. + /// + public bool IsStatic { get; internal set; } + + /// + /// Gets the path to the file that this action was declared in. + /// + public string? SourceFileName { get; internal set; } + + /// + /// The syntax node for the method declaration associated with this action. + /// + public SyntaxNode? MethodDeclarationSyntax { get; internal set; } + + // The names of the methods that register commands and functions + private const string AddCommandHandlerMethodName = "AddCommandHandler"; + private const string AddFunctionMethodName = "AddFunction"; + private const string RegisterFunctionDeclarationName = "RegisterFunctionDeclaration"; + + /// + /// The list of parameters that this action takes. + /// + public List Parameters = new List(); + + public string? ReturnDescription; + public string YarnReturnTypeString => this.MethodSymbol.ReturnType.GetYarnTypeString(); + + public bool ContainsErrors = false; + + public string ToJSON() + { + var result = new Dictionary(); + + result["yarnName"] = this.Name; + result["definitionName"] = this.MethodName; + result["fileName"] = this.SourceFileName; + if (!string.IsNullOrEmpty(this.Description)) + { + result["documentation"] = this.Description; + } + result["language"] = "csharp"; + result["async"] = this.AsyncType != AsyncType.Sync; + + result["containsErrors"] = this.ContainsErrors; + + if (this.Declaration != null) + { + var location = this.Declaration.GetLocation().GetLineSpan(); + + var startPosition = new Dictionary() + { + {"line", location.StartLinePosition.Line}, + {"character", location.StartLinePosition.Character}, + }; + var endPosition = new Dictionary() + { + {"line", location.EndLinePosition.Line}, + {"character", location.EndLinePosition.Character}, + }; + result["location"] = new Dictionary>() + { + {"start", startPosition}, + {"end", endPosition}, + }; + } + + result["parameters"] = new List>(this.Parameters.Select(p => + { + var paramObject = new Dictionary(); + + paramObject["name"] = p.Name; + if (!string.IsNullOrEmpty(p.Description)) + { + paramObject["documentation"] = p.Description; + } + if (!string.IsNullOrEmpty(p.DefaultValueString)) + { + paramObject["defaultValue"] = p.DefaultValueString; + } + paramObject["isParamsArray"] = p.IsParamsArray; + + // there are two special cases for parameters + // if it is a subclass of UnityEngine.Component or MonoBehaviour we additionally add the subtype + // this is used by the editor later on to let the writer know WHERE the command will be going + // otherwise we just add the Yarn type of the parameter + if (p.Type.BaseType?.Name == "MonoBehaviour" || p.Type.BaseType?.Name == "Component") + { + paramObject["type"] = "instance"; + paramObject["subtype"] = p.Type.Name; + } + else + { + // there are two special case of the regular types: + // if you are a string and attributed as a node parameter you get declared as being a node type + // if you have an enum attribute it gets declared as an enum and it has the subtype as defined in the enum attribute + + var isANodeType = p.Attributes?.Count(a => a.AttributeClass?.Name == "YarnNodeParameterAttribute") > 0; + var isAnEnum = p.Attributes?.Count(a => a.AttributeClass?.Name == "YarnEnumParameterAttribute") > 0; + + if (isANodeType && p.Type.SpecialType == SpecialType.System_String) + { + paramObject["type"] = "node"; + } + else if (isAnEnum) + { + var subtype = "any"; + var attribute = p.Attributes?.Where(a => a.AttributeClass?.Name == "YarnEnumParameterAttribute").First(); + if (attribute != null && attribute.ConstructorArguments.Count() > 0) + { + var enumType = attribute.ConstructorArguments[0]; + if (enumType.Type?.SpecialType == SpecialType.System_String) + { + subtype = enumType.Value as string ?? p.YarnTypeString; + } + } + + paramObject["type"] = "enum"; + paramObject["subtype"] = subtype; + + } + else + { + paramObject["type"] = p.YarnTypeString; + } + } + + return paramObject; + }).ToArray()); + + if (this.Type == ActionType.Function) + { + var retvrn = new Dictionary(); + retvrn["type"] = this.YarnReturnTypeString; + + if (!string.IsNullOrWhiteSpace(this.ReturnDescription)) + { + retvrn["description"] = this.ReturnDescription!; + } + result["return"] = retvrn; + } + + return Yarn.Unity.Editor.Json.Serialize(result); + } + + public List Validate(Compilation compilation, ILogger? logger) + { + logger?.WriteLine($"Beginning validation"); + var diagnostics = new List(); + if (this.MethodDeclarationSyntax == null) + { + // No declaration syntax - we have nowhere to attach any diagnostics to + return diagnostics; + } + + Location diagnosticLocation; + string identifier; + + if (this.Declaration is MethodDeclarationSyntax methodDeclarationSyntax) + { + diagnosticLocation = methodDeclarationSyntax.Identifier.GetLocation(); + identifier = methodDeclarationSyntax.Identifier.ToString(); + } + else + { + diagnosticLocation = this.MethodDeclarationSyntax.GetLocation(); + identifier = "(anonymous function)"; + } + + // Commands are parsed as whitespace, so spaces in the command name + // would render the command un-callable. + if (Name.Any(x => Char.IsWhiteSpace(x))) + { + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1002ActionMethodsMustHaveAValidName, this.MethodDeclarationSyntax.GetLocation(), this.Name)); + } + + if (this.Name == null) + { + throw new NullReferenceException("Action name is null"); + } + + if (this.MethodSymbol == null) + { + throw new NullReferenceException($"Method symbol for {Name} is null"); + } + + // Actions that are registered via an attribute must be publicly + // accessible + if (this.DeclarationType == DeclarationType.Attribute) + { + if (MethodSymbol.DeclaredAccessibility != Accessibility.Public) + { + // The method is not public + diagnostics.Add(Diagnostic.Create( + Diagnostics.YS1001ActionMethodsMustBePublic, + diagnosticLocation, identifier, MethodSymbol.DeclaredAccessibility)); + } + else + { + var containingType = MethodSymbol.ContainingType; + + while (containingType != null) + { + if (containingType.DeclaredAccessibility != Accessibility.Public) + { + // The method is public, but it's within a type that + // is not + var typeName = containingType.Name ?? "(anonymous)"; + diagnostics.Add(Diagnostic.Create( + Diagnostics.YS1001ActionMethodsMustBePublic, + diagnosticLocation, identifier, typeName, containingType.DeclaredAccessibility)); + break; + } + containingType = containingType.ContainingType; + } + + } + } + + switch (Type) + { + case ActionType.Invalid: + { + var actionAttributes = MethodSymbol.GetAttributes().Where(attr => Analyser.IsAttributeYarnCommand(attr)); + + var count = actionAttributes.Count(); + + if (count != 1) + { + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1005ActionMethodsMustHaveOneActionAttribute, diagnosticLocation, 0)); + } + else + { + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1000UnknownError, diagnosticLocation, "Method marked as 'not an action' but it had one attribute")); + } + } + break; + + case ActionType.Command: + diagnostics.AddRange(ValidateCommand(compilation, logger)); + break; + + case ActionType.Function: + diagnostics.AddRange(ValidateFunction(compilation, logger)); + break; + + default: + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1000UnknownError, diagnosticLocation, $"Internal error: invalid type {Type}")); + break; + } + + return diagnostics; + } + + private IEnumerable ValidateFunction(Compilation compilation, ILogger? logger) + { + string identifier; + Location returnTypeLocation; + Location identifierLocation; + + if (this.Declaration == null) + { + // No declaration - we can't attach any diagnostics + yield break; + } + + if (this.Declaration is MethodDeclarationSyntax methodDeclarationSyntax) + { + identifierLocation = methodDeclarationSyntax.Identifier.GetLocation(); + returnTypeLocation = methodDeclarationSyntax.ReturnType.GetLocation(); + identifier = methodDeclarationSyntax.Identifier.ToString(); + } + else + { + identifierLocation = Declaration.GetLocation(); + returnTypeLocation = this.Declaration.GetLocation(); + identifier = "(anonymous function)"; + } + + if (this.MethodSymbol == null) + { + throw new NotImplementedException("Todo: handle case where action's method is not a IMethodSymbol"); + } + + // Functions must be static if they're declared via attributes + if (this.DeclarationType == DeclarationType.Attribute + && this.MethodSymbol.MethodKind == MethodKind.Ordinary + && this.MethodSymbol.IsStatic == false) + { + yield return Diagnostic.Create(Diagnostics.YS1006YarnFunctionsMustBeStatic, identifierLocation); + } + + logger?.Inc(); + logger?.WriteLine($"Validating {identifier} as a function"); + var paramDiags = ValidateParameters(compilation, logger); + foreach (var p in paramDiags) + { + yield return p; + } + + // Functions must return a number, string, or bool + var returnTypeSymbol = this.MethodSymbol.ReturnType; + + logger?.Dec(); + switch (returnTypeSymbol.SpecialType) + { + case SpecialType.System_Boolean: + case SpecialType.System_SByte: + case SpecialType.System_Byte: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + case SpecialType.System_Decimal: + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_String: + break; + default: + yield return Diagnostic.Create(Diagnostics.YS1004FunctionMethodsMustHaveAValidReturnType, returnTypeLocation, identifier, returnTypeSymbol.ToString()); + break; + + } + } + + // validates the parameters are correct + private List ValidateParameters(Compilation compilation, ILogger? logger) + { + logger?.Inc(); + List diagnostics = new List(); + ParameterListSyntax? parameterList = null; + string? identifier = null; + + if (this.MethodDeclarationSyntax is MethodDeclarationSyntax methodDeclaration) + { + identifier = methodDeclaration.Identifier.ToString(); + logger?.WriteLine($"identified {identifier} as a method"); + parameterList = methodDeclaration.ParameterList; + } + else if (this.MethodDeclarationSyntax is LocalFunctionStatementSyntax localFunctionStatement) + { + identifier = localFunctionStatement.Identifier.ToString(); + logger?.WriteLine($"identified {identifier} as a local function"); + parameterList = localFunctionStatement.ParameterList; + } + else if (this.MethodDeclarationSyntax is LambdaExpressionSyntax lambdaExpression) + { + logger?.WriteLine("identifed the action as a lambda."); + var actionLocation = lambdaExpression.GetLocation(); + + if (lambdaExpression is SimpleLambdaExpressionSyntax) + { + logger?.WriteLine("The action is a simple lambda, validations do not apply here, skipping this action."); + logger?.Dec(); + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1012ActionIsALambda, actionLocation)); + return diagnostics; + } + + if (lambdaExpression is ParenthesizedLambdaExpressionSyntax pls) + { + logger?.WriteLine("The action is a parenthesized lambda, can perform some validation."); + + identifier = "(lambda expression)"; + parameterList = pls.ParameterList; + + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1012ActionIsALambda, actionLocation)); + } + } + + if (parameterList == null || parameterList.Parameters.Count() == 0) + { + logger?.WriteLine($"{identifier} has no parameters, ignoring"); + logger?.Dec(); + return diagnostics; + } + + logger?.WriteLine($"Will be checking {parameterList.Parameters.Count()} parameters"); + + int parameterIndex = 0; + int parameterCount = parameterList.Parameters.Count; + foreach (var parameter in parameterList.Parameters) + { + parameterIndex += 1; + logger?.Inc(); + if (parameter.Type == null) + { + logger?.WriteLine($"{parameter.ToFullString()} has no type, ignoring validation?"); + logger?.Dec(); + continue; + } + + var model = compilation.GetSemanticModel(parameter.SyntaxTree); + var typeInfo = model.GetTypeInfo(parameter.Type).Type; + + var parameterName = model.GetDeclaredSymbol(parameter)?.Name ?? "(UNKNOWN)"; + logger?.WriteLine($"Validating {parameterName}"); + + if (typeInfo == null) + { + logger?.WriteLine($"Unable to determine typeinfo of {parameterName} ignoring validation?"); + logger?.Dec(); + continue; + } + + var symbol = model.GetDeclaredSymbol(parameter); + if (symbol == null) + { + logger?.WriteLine($"Unable to determine the declared symbol for {parameterName}, skipping validation"); + logger?.Dec(); + continue; + } + + // Params arrays or arrays that are the final parameter make + // that parameter variadic in Yarn Spinner. Check that the + // element type of that array is of the right type. + if (symbol.Type is IArrayTypeSymbol arrayTypeSymbol + && (symbol.IsParams || parameterIndex == parameterCount)) + { + var subtype = arrayTypeSymbol.ElementType; + if (subtype.GetYarnTypeString() == "any") + { + logger?.WriteLine($"{parameterName} is a parameter array of non Yarn compatible types!"); + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1008ActionsParamsArraysMustBeOfYarnTypes, parameter.GetLocation(), parameterName, subtype.Name)); + } + } + else + { + if (typeInfo.GetYarnTypeString() == "any" && typeInfo.BaseType?.Name != "Component") + { + // we have an invalid type + logger?.WriteLine($"{parameterName} is an invalid type for use in a Yarn action"); + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1011ActionsParameterIsAnIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name)); + } + } + + foreach (var attribute in symbol.GetAttributes()) + { + // this attribute is an enum parameter + if (attribute.AttributeClass?.Name == "YarnEnumParameterAttribute") + { + if (typeInfo.GetYarnTypeString() == "any") + { + logger?.WriteLine($"{parameterName} is attributed as an enum but isn't a Yarn compatible type!"); + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1009ActionsEnumAttributedParameterIsOfIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name)); + } + } + if (attribute.AttributeClass?.Name == "YarnNodeParameterAttribute") + { + if (typeInfo.GetYarnTypeString() != "string") + { + logger?.WriteLine($"{parameterName} is attributed as a node but isn't a string!"); + diagnostics.Add(Diagnostic.Create(Diagnostics.YS1010ActionsNodeAttributedParameterIsOfIncompatibleType, parameter.GetLocation(), parameterName, typeInfo.Name)); + } + } + } + logger?.Dec(); + } + + logger?.Dec(); + return diagnostics; + } + + private IEnumerable ValidateCommand(Compilation compilation, ILogger? logger) + { + logger?.Inc(); + if (MethodSymbol == null) + { + logger?.Dec(); + throw new NullReferenceException("Method symbol is null"); + } + + List validCommandReturnTypes = new List { + compilation.GetTypeByMetadataName("UnityEngine.Coroutine"), + compilation.GetTypeByMetadataName("System.Collections.IEnumerator"), + compilation.GetSpecialType(SpecialType.System_Void), + } + .NonNull(throwIfAnyNull: true) + .ToList(); + + List validTaskTypes = new List { + compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"), + compilation.GetTypeByMetadataName("Cysharp.Threading.Tasks.UniTask"), + compilation.GetTypeByMetadataName("UnityEngine.Awaitable"), + compilation.GetTypeByMetadataName("Yarn.Unity.YarnTask"), + }.NonNull(throwIfAnyNull: false) + .ToList(); + + // Explicitly ban 'string' as a return type - strings implement + // IEnumerator, but they're not coroutines. We'll need to manually + // exclude this. + List knownInvalidCommandReturnTypes = new List { + compilation.GetSpecialType(SpecialType.System_String), + } + .NonNull(throwIfAnyNull: true) + .ToList(); + + // Functions must return void, IEnumerator, Coroutine, or an awaitable type + var returnTypeSymbol = MethodSymbol.ReturnType; + + Location returnTypeLocation; + string identifier; + string returnTypeName; + if (this.MethodDeclarationSyntax is MethodDeclarationSyntax methodDeclaration) + { + returnTypeLocation = methodDeclaration.ReturnType.GetLocation(); + identifier = methodDeclaration.Identifier.ToString(); + returnTypeName = methodDeclaration.ReturnType.ToString(); + } + else if (this.MethodDeclarationSyntax is LocalFunctionStatementSyntax localFunctionStatement) + { + returnTypeLocation = localFunctionStatement.ReturnType.GetLocation(); + identifier = localFunctionStatement.Identifier.ToString(); + returnTypeName = localFunctionStatement.ReturnType.ToString(); + } + else if (this.MethodDeclarationSyntax is LambdaExpressionSyntax lambdaExpression) + { + returnTypeLocation = lambdaExpression.GetLocation(); + identifier = "(lambda expression)"; + returnTypeName = returnTypeSymbol.Name; + } + else + { + logger?.Dec(); + throw new InvalidOperationException($"Expected decl for {this.Name} ({this.SourceFileName}) was of unexpected type {this.MethodDeclarationSyntax?.GetType().Name ?? "null"}"); + } + + logger?.WriteLine($"Validating {identifier} as a command"); + + var paramDiags = ValidateParameters(compilation, logger); + foreach (var p in paramDiags) + { + yield return p; + } + + var typeIsKnownValid = validCommandReturnTypes.Contains(returnTypeSymbol) + || validTaskTypes.Contains(returnTypeSymbol); + var typeIsKnownInvalid = knownInvalidCommandReturnTypes.Contains(returnTypeSymbol); + + var returnTypeIsValid = typeIsKnownValid && !typeIsKnownInvalid; + logger?.Dec(); + + if (returnTypeIsValid == false) + { + yield return Diagnostic.Create(Diagnostics.YS1003CommandMethodsMustHaveAValidReturnType, + returnTypeLocation, + identifier, + returnTypeName); + } + } + + public StatementSyntax GetRegistrationSyntax(string dialogueRunnerVariableName = "dialogueRunner") + { + if (MethodSymbol == null) + { + throw new NullReferenceException("Method symbol is null"); + } + if (Name == null) + { + throw new NullReferenceException("Action name is null"); + } + string registrationMethodName; + switch (Type) + { + case ActionType.Command: + registrationMethodName = AddCommandHandlerMethodName; + break; + case ActionType.Function: + registrationMethodName = AddFunctionMethodName; + break; + default: + throw new InvalidOperationException($"Action {Name} is not a valid action"); + } + + SimpleNameSyntax nameSyntax; + + // Get any parameters we have for this method as a sequence of type + // symbols. We'll use that when building the call to + // AddCommandHandler/Function. + var parameterTypes = (MethodSymbol as IMethodSymbol)?.Parameters.Select(p => p.Type) ?? Enumerable.Empty(); + + var typeArguments = parameterTypes.Select(t => + { + return SyntaxFactory.ParseTypeName(t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + }); + + // If this is a function, we also need to include the return type in + // this list. + if (Type == ActionType.Function) + { + var returnType = (MethodSymbol as IMethodSymbol)?.ReturnType ?? throw new InvalidOperationException($"Action {Name} has type {ActionType.Function}, but its return type is null."); + + typeArguments = typeArguments.Append(SyntaxFactory.ParseTypeName(returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + } + + if (typeArguments.Any() && MethodSymbol?.IsStatic == true) + { + // This method needs to be specified with type arguments, so + // we'll need to call the appropriate generic version of + // AddCommandHandler/Function that takes type parameters. Create + // a new GenericName for AddCommandHandler/Function and provide + // it with the type parameter list that we just built. + + nameSyntax = SyntaxFactory.GenericName( + SyntaxFactory.Identifier(registrationMethodName), + SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(typeArguments)) + ); + } + else + { + // This method doesn't need to specify any type parameters, so + // we can just use the identifier name. + nameSyntax = SyntaxFactory.IdentifierName(registrationMethodName); + } + + // Create the expression that refers to the + // 'AddCommandHandler/Function' instance method on the dialogue + // runner variable name we were provided. + var addCommandHandlerExpression = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(dialogueRunnerVariableName), + SyntaxFactory.Token(SyntaxKind.DotToken), + nameSyntax + ); + + ExpressionSyntax methodReferenceExpression = GetReferenceSyntaxForRegistration(); + + var arguments = SyntaxFactory.ArgumentList().AddArguments(new[]{ + SyntaxFactory.Argument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(this.Name) + ) + ), + SyntaxFactory.Argument(methodReferenceExpression) + .WithLeadingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " ")), + }); + + var invocationExpressionSyntax = SyntaxFactory.InvocationExpression(addCommandHandlerExpression, arguments); + + var invocationStatement = SyntaxFactory.ExpressionStatement(invocationExpressionSyntax); + + return invocationStatement; + } + + public ExpressionSyntax GetReferenceSyntaxForRegistration() + { + // Create an expression that refers to the type that contains the + // method we're registering. + var containingTypeExpression = SyntaxFactory.ParseName(MethodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + + // Now use that to create an expression that refers to _this method group_ + // on _that type_. + var methodReference = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + containingTypeExpression, + SyntaxFactory.IdentifierName(MethodSymbol.Name) + ); + + if (IsStatic) + { + + // If the method is static, we can use the reference to the method directly. + return methodReference; + + } + else + { + // If the method is not static, we must create a MethodInfo for this method, like this: + // typeof(ContainingType) + // .GetMethod(nameof(ContainingType.Method), + // new[] { typeof(MethodParam1), typeof(MethodParam2)} ) + + // Create an expression that gets a MethodInfo for the action's method. + + var typeOfContainingTypeExpression = SyntaxFactory.TypeOfExpression(containingTypeExpression); + + const string nameOfIdentifier = "nameof"; + const string getMethodIdentifier = "GetMethod"; + + var typeOfMethodParameters = MethodSymbol.Parameters.Select(p => + { + string typeName = p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + TypeSyntax type = SyntaxFactory.ParseTypeName(typeName); + return SyntaxFactory.TypeOfExpression(type); + }); + + ExpressionSyntax nameOfMethod; + + if (MethodSymbol.DeclaredAccessibility != Accessibility.Public) + { + // The method is not public, so we can't use nameof() on it, + // because it would cause a compiler error. Instead, we'll have to + // refer to the method by name. + nameOfMethod = SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(MethodName ?? MethodSymbol.Name) + ); + } + else + { + // The method is public, so we can use nameof() to refer to + // it in a more durable way. + + nameOfMethod = SyntaxFactory.InvocationExpression( + SyntaxFactory.ParseName(nameOfIdentifier), + SyntaxFactory.ArgumentList( + SyntaxFactory.SeparatedList( + new[] { + SyntaxFactory.Argument(methodReference) + } + ) + ) + ); + } + + + var arrayOfTypeParameters = SyntaxFactory.ArrayCreationExpression( + SyntaxFactory.ArrayType( + SyntaxFactory.ParseTypeName("System.Type"), + SyntaxFactory.List( + new[] { + SyntaxFactory.ArrayRankSpecifier( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.OmittedArraySizeExpression() + ) + ) + } + ) + ), + SyntaxFactory.InitializerExpression( + SyntaxKind.ArrayInitializerExpression, + + SyntaxFactory.SeparatedList( + typeOfMethodParameters + ) + ) + ); + + var getMethod = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + typeOfContainingTypeExpression, + SyntaxFactory.IdentifierName(getMethodIdentifier) + ); + + var getMethodArguments = SyntaxFactory.ArgumentList( + SyntaxFactory.SeparatedList( + new[] { + SyntaxFactory.Argument(nameOfMethod), + SyntaxFactory.Argument(arrayOfTypeParameters) + } + ) + ); + + var getMethodInvocation = SyntaxFactory.InvocationExpression(getMethod, getMethodArguments); + + return getMethodInvocation; + } + } + + public StatementSyntax GetFunctionDeclarationSyntax(string dialogueRunnerVariableName = "dialogueRunner") + { + var typeOfMethodReturn = SyntaxFactory.TypeOfExpression(SyntaxFactory.ParseTypeName(MethodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + var typeOfMethodParameters = MethodSymbol.Parameters.Select(p => + { + string typeName = p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + TypeSyntax type = SyntaxFactory.ParseTypeName(typeName); + return SyntaxFactory.TypeOfExpression(type); + }); + + var arrayOfTypeParameters = SyntaxFactory.ArrayCreationExpression( + SyntaxFactory.ArrayType( + SyntaxFactory.ParseTypeName("System.Type"), + SyntaxFactory.List( + new[] { + SyntaxFactory.ArrayRankSpecifier( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.OmittedArraySizeExpression() + ) + ) + } + ) + ), + SyntaxFactory.InitializerExpression( + SyntaxKind.ArrayInitializerExpression, + + SyntaxFactory.SeparatedList( + typeOfMethodParameters + ) + ) + ); + + var argumentsToRegisterCall = SyntaxFactory.ArgumentList().AddArguments(new[]{ + SyntaxFactory.Argument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(this.Name) + ) + ), + SyntaxFactory.Argument(typeOfMethodReturn), + SyntaxFactory.Argument(arrayOfTypeParameters) + }); + + // Create the expression that refers to the + // 'RegisterFunctionDeclaration' instance method on the dialogue + // runner variable name we were provided. + var registerFunctionMethodAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(dialogueRunnerVariableName), + SyntaxFactory.Token(SyntaxKind.DotToken), + SyntaxFactory.IdentifierName(RegisterFunctionDeclarationName) + ); + + var registerFunctionMethodInvocation = SyntaxFactory.InvocationExpression(registerFunctionMethodAccess, argumentsToRegisterCall); + + var invocationStatement = SyntaxFactory.ExpressionStatement(registerFunctionMethodInvocation); + + return invocationStatement; + } + } +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs.meta new file mode 100644 index 00000000..1a0c9743 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ad1e0218baeab43be922ae40dbfb3ade +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionSourceCodeGenerator.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionSourceCodeGenerator.cs new file mode 100644 index 00000000..eb5a80b8 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionSourceCodeGenerator.cs @@ -0,0 +1,98 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using System.Linq; +using UnityEditor; +using UnityEngine; +using Yarn.Unity; + +namespace Yarn.Unity.Editor +{ + public static class ActionSourceCodeGenerator + { + + /// + /// Get a path in the current project that can be used for storing + /// manually-generated Yarn Action registration code. + /// + /// + /// This property checks to see if a file exists in the Assets folder + /// that is both named "YarnActionRegistration.cs", and contains a + /// marker indicating that it was generated by Yarn Spinner's code + /// generation systems. If this is found, the path to the file is + /// returned. Otherwise, the path + /// Assets/YarnActionRegistration.cs is returned. + /// + public static string GeneratedSourcePath + { + get + { + const string YarnRegistrationFileName = "YarnActionRegistration.cs"; + const string DefaultOutputFilePath = "Assets/" + YarnRegistrationFileName; + + // Note the lack of a closing parenthesis in this string - we + // only want to check to see if it was generated by + // "YarnActionAnalyzer", not any specific version of that + // analyzer + const string YarnGeneratedCodeSignature = "GeneratedCode(\"YarnActionAnalyzer\""; + + var existingFile = System.IO.Directory.EnumerateFiles(System.Environment.CurrentDirectory, YarnRegistrationFileName, System.IO.SearchOption.AllDirectories).FirstOrDefault(); + + if (existingFile == null) + { + return DefaultOutputFilePath; + } + else + { + try + { + var text = System.IO.File.ReadAllText(existingFile); + return text.Contains(YarnGeneratedCodeSignature) + ? existingFile + : DefaultOutputFilePath; + } + catch (System.Exception e) + { + // Something happened while checking the file. Return + // our default, and log that we encountered a problem. + Debug.LogWarning($"Can't check to see if {existingFile} is a valid action registration script, using {DefaultOutputFilePath} instead: {e}"); + return DefaultOutputFilePath; + } + } + + } + } + + /// + /// Generates and imports a C# source code file in the project + /// containing Yarn Action registration code at the path indicated by + /// . + /// + /// + /// This method should not be called in projects where Unity has support + /// for source generators (i.e. Unity 2021.2 and later). + /// + public static void GenerateYarnActionSourceCode() + { + var analysis = new Yarn.Unity.ActionAnalyser.Analyser("Assets"); + try + { + var actions = analysis.GetActions(); + var source = Yarn.Unity.ActionAnalyser.Analyser.GenerateRegistrationFileSource(actions); + + var path = GeneratedSourcePath; + + System.IO.File.WriteAllText(path, source); + UnityEditor.AssetDatabase.ImportAsset(path); + + Debug.Log($"Generated Yarn command and function registration code at {path}"); + } + catch (Yarn.Unity.ActionAnalyser.AnalyserException e) + { + Debug.LogError($"Error generating source code: " + e.InnerException.ToString()); + } + } + + } +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionSourceCodeGenerator.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionSourceCodeGenerator.cs.meta new file mode 100644 index 00000000..2a97a97f --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionSourceCodeGenerator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dabf0558de47a486199baa9e568783ce +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs new file mode 100644 index 00000000..0b255648 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionsRegistrationGenerator~/ActionsGenerator.cs @@ -0,0 +1,625 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Yarn.Unity.ActionAnalyser; +using YarnAction = Yarn.Unity.ActionAnalyser.Action; + +#nullable enable + +[Generator] +public class ActionRegistrationSourceGenerator : ISourceGenerator +{ + const string YarnSpinnerUnityAssemblyName = "YarnSpinner.Unity"; + const string DebugLoggingPreprocessorSymbol = "YARN_SOURCE_GENERATION_DEBUG_LOGGING"; + const string IncludeTestCommands = "YARN_INCLUDE_TEST_COMMANDS"; + const string MinimumUnityVersionPreprocessorSymbol = "UNITY_2021_2_OR_NEWER"; + + public static string? GetProjectRoot(GeneratorExecutionContext context) + { + // We need to know if the settings are configured to not perform codegen + // to link attributed methods. This is kinda annoying because the path + // root of the project settings and the root path of this process are + // *very* different. So, what we do is we use the included Compilation + // Assembly additional file that Unity gives us. This file, if opened, + // has the path of the Unity project, which we can then use to get the + // settings. If any stage of this fails, then we bail out and assume + // that codegen is desired. + + // Try and find any additional files passed to the context + if (!context.AdditionalFiles.Any()) + { + return null; + } + + // One of those files is (AssemblyName).[Unity]AdditionalFile.txt, and it + // contains the path to the project + var relevantFiles = context.AdditionalFiles.Where( + i => i.Path.Contains($"{context.Compilation.AssemblyName}.AdditionalFile.txt") + || i.Path.Contains($"{context.Compilation.AssemblyName}.UnityAdditionalFile.txt") + ); + + if (!relevantFiles.Any()) + { + return null; + } + + var assemblyRelevantFile = relevantFiles.First(); + + // The file needs to exist on disk + if (!File.Exists(assemblyRelevantFile.Path)) + { + return null; + } + + try + { + // Attempt to read it - it should contain the path to the project directory + var projectPath = File.ReadAllText(assemblyRelevantFile.Path); + if (Directory.Exists(projectPath)) + { + // If this directory exists, we're done + return projectPath; + } + else + { + return null; + } + } + catch (IOException) + { + // We encountered a problem while testing + return null; + } + } + + public void Execute(GeneratorExecutionContext context) + { + using var output = GetOutput(context); + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + output.WriteLine(DateTime.Now); + + Yarn.Unity.Editor.YarnSpinnerProjectSettings? settings = null; + var projectPath = GetProjectRoot(context); + + if (projectPath != null) + { + try + { + var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerProjectSettingsPath); + output.WriteLine($"Attempting to read settings file at {fullPath}"); + + settings = Yarn.Unity.Editor.YarnSpinnerProjectSettings.GetOrCreateSettings(projectPath, output); + if (!settings.automaticallyLinkAttributedYarnCommandsAndFunctions) + { + output.WriteLine("Skipping codegen due to settings."); + return; + } + } + catch (Exception e) + { + output.WriteLine($"Unable to determine Yarn settings, settings values will be ignored and codegen will occur: {e.Message}"); + } + } + else + { + output.WriteLine($"Unable to determine project location on disk. Settings values will be ignored and codegen will occur"); + } + + bool hasCriticalActionErrors = false; + try + { + output.WriteLine("Source code generation for assembly " + context.Compilation.AssemblyName); + + if (context.AdditionalFiles.Any()) + { + output.WriteLine($"Additional files:"); + foreach (var item in context.AdditionalFiles) + { + output.WriteLine(" " + item.Path); + } + } + + output.WriteLine("Referenced assemblies for this compilation:"); + foreach (var referencedAssembly in context.Compilation.ReferencedAssemblyNames) + { + output.WriteLine(" - " + referencedAssembly.Name); + } + + bool compilationReferencesYarnSpinner = context.Compilation.ReferencedAssemblyNames + .Any(name => name.Name == YarnSpinnerUnityAssemblyName); + + if (compilationReferencesYarnSpinner == false) + { + // This compilation doesn't reference YarnSpinner.Unity. Any + // code that we generate that references symbols in that + // assembly won't work. + output.WriteLine($"Assembly {context.Compilation.AssemblyName} doesn't reference {YarnSpinnerUnityAssemblyName}. Not generating any code for it."); + return; + } + + output.WriteLine("Preprocessor Symbols: "); + foreach (var symbol in context.ParseOptions.PreprocessorSymbolNames) + { + output.WriteLine("- " + symbol); + } + + // Don't generate source code if we're not targeting at least Unity + // 2021.2. (Unity will not invoke this DLL as a source code + // generator until at least this version, but other tools like + // OmniSharp might.) + if (!context.ParseOptions.PreprocessorSymbolNames.Contains(MinimumUnityVersionPreprocessorSymbol)) + { + output.WriteLine($"Not generating code for assembly {context.Compilation.AssemblyName} because this assembly is not being built for Unity 2021.2 or newer"); + return; + } + + // Don't generate source code for certain Yarn Spinner provided + // assemblies - these always manually register any actions in them. + var prefixesToIgnore = new List() + { + "YarnSpinner.Unity", + "YarnSpinner.Editor", + }; + + // But DO generate source code for the Samples assembly and the Test assembly + var prefixesToKeep = new List() + { + "YarnSpinner.Unity.Samples", + }; + + // Additionally, if we're building for unit tests, include the Yarn + // Spinner unit tests assembly. + if (context.ParseOptions.PreprocessorSymbolNames.Contains(IncludeTestCommands)) + { + prefixesToKeep.Add("YarnSpinner.Unity.Tests"); + } + + if (context.Compilation.AssemblyName == null) + { + output.WriteLine("Not generating registration code, because the provided AssemblyName is null"); + return; + } + + if (prefixesToIgnore.Any(prefix => context.Compilation.AssemblyName.StartsWith(prefix)) && !prefixesToKeep.Any(prefix => context.Compilation.AssemblyName.StartsWith(prefix))) + { + output.WriteLine($"Not generating registration code for {context.Compilation.AssemblyName}: we've been told to exclude it, because its name begins with one of these prefixes: {string.Join(", ", prefixesToIgnore)}"); + return; + } + + if (!(context.Compilation is CSharpCompilation compilation)) + { + // This is not a C# compilation, so we can't do analysis. + output.WriteLine($"Stopping code generation because compilation is not a {nameof(CSharpCompilation)}."); + return; + } + + var actions = new List(); + foreach (var tree in compilation.SyntaxTrees) + { + actions.AddRange(Analyser.GetActions(compilation, tree, output)); + } + + if (actions.Count() == 0) + { + output.WriteLine($"Didn't find any Yarn Actions in {context.Compilation.AssemblyName}. Not generating any source code for it."); + return; + } + + // validating and logging all the actions + foreach (var action in actions) + { + if (action == null) + { + output.WriteLine($"Action is null??"); + continue; + } + + var diagnostics = action.Validate(compilation, output); + foreach (var diagnostic in diagnostics) + { + context.ReportDiagnostic(diagnostic); + if (diagnostic.Severity == DiagnosticSeverity.Warning || diagnostic.Severity == DiagnosticSeverity.Error) + { + output.WriteLine($"Flagging '{action.Name}' ({action.MethodName}): {diagnostic}"); + action.ContainsErrors = true; + + if (diagnostic.Severity == DiagnosticSeverity.Error) + { + hasCriticalActionErrors = true; + } + } + } + + // Commands are parsed as whitespace, so spaces in the command name + // would render the command un-callable. + if (action.Name.Any(x => Char.IsWhiteSpace(x))) + { + var descriptor = new DiagnosticDescriptor( + "YS1002", + $"Yarn {action.Type} methods must have a valid name", + "YarnCommand and YarnFunction methods follow existing ID rules for Yarn. \"{0}\" is invalid.", + "Yarn Spinner", + DiagnosticSeverity.Warning, + true, + "[YarnCommand] and [YarnFunction] attributed methods must follow Yarn ID rules so that Yarn scripts can reference them.", + "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions"); + context.ReportDiagnostic(Microsoft.CodeAnalysis.Diagnostic.Create( + descriptor, + action.Declaration?.GetLocation(), + action.Name + )); + action.ContainsErrors = true; + output.WriteLine($"Action {action.MethodIdentifierName} will be flagged due to it's name {action.Name}"); + continue; + } + + output.WriteLine($"Action {action.Name}: {action.SourceFileName}:{action.Declaration?.GetLocation()?.GetLineSpan().StartLinePosition.Line} ({action.Type})"); + } + + if (hasCriticalActionErrors) + { + stopwatch.Stop(); + output.WriteLine($"Critical issues were encountered in the actions, aborting code generation, stopping analysis after {stopwatch.Elapsed.TotalMilliseconds}ms"); + return; + } + + output.Write($"Generating source code..."); + + var source = Analyser.GenerateRegistrationFileSource(actions); + + output.WriteLine($"Done."); + + SourceText sourceText = SourceText.From(source, Encoding.UTF8); + + output.Write($"Writing generated source..."); + + DumpGeneratedFile(context, source); + + output.WriteLine($"Done."); + + context.AddSource($"YarnActionRegistration-{compilation.AssemblyName}.Generated.cs", sourceText); + + if (settings != null) + { + if (settings.generateYSLSFile) + { + output.Write($"Generating ysls..."); + // generating the ysls + + IEnumerable commandJSON = actions.Where(a => a.Type == ActionType.Command).Select(a => a.ToJSON()); + IEnumerable functionJSON = actions.Where(a => a.Type == ActionType.Function).Select(a => a.ToJSON()); + + var ysls = "{" + + @"""version"":2," + + $@"""commands"":[{string.Join(",", commandJSON)}]," + + $@"""functions"":[{string.Join(",", functionJSON)}]" + + "}"; + + output.WriteLine($"Done."); + + if (!string.IsNullOrEmpty(projectPath)) + { + output.Write($"Writing generated ysls..."); + + var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerAssemblyGeneratedYSLSPath(compilation.AssemblyName)); + try + { + System.IO.File.WriteAllText(fullPath, ysls); + output.WriteLine($"Done."); + } + catch (Exception e) + { + output.WriteLine($"Unable to write ysls to disk: {e.Message}"); + } + } + else + { + output.WriteLine("unable to identify project path, ysls will not be written to disk"); + } + } + else + { + output.WriteLine($"skipping ysls generation due to settings"); + } + } + else + { + output.WriteLine($"skipping ysls generation due to settings not being found"); + } + + stopwatch.Stop(); + output.WriteLine($"Source code generation completed in {stopwatch.Elapsed.TotalMilliseconds}ms"); + return; + + } + catch (Exception e) + { + output.WriteLine($"{e}"); + } + } + + private MethodDeclarationSyntax GenerateLoggingMethod(string methodName, string sourceExpression, string prefix) + { + return SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType( + SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + SyntaxFactory.Identifier(methodName)) + .WithModifiers( + SyntaxFactory.TokenList( + new[]{ + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword)})) + .WithBody( + SyntaxFactory.Block( + SyntaxFactory.LocalDeclarationStatement( + SyntaxFactory.VariableDeclaration( + SyntaxFactory.GenericName( + SyntaxFactory.Identifier("IEnumerable")) + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.PredefinedType( + SyntaxFactory.Token(SyntaxKind.StringKeyword)))))) + .WithVariables( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.VariableDeclarator( + SyntaxFactory.Identifier("source")) + .WithInitializer( + SyntaxFactory.EqualsValueClause( + SyntaxFactory.ParseExpression(sourceExpression)))))), + SyntaxFactory.LocalDeclarationStatement( + SyntaxFactory.VariableDeclaration( + SyntaxFactory.IdentifierName( + SyntaxFactory.Identifier( + SyntaxFactory.TriviaList(), + SyntaxKind.VarKeyword, + "var", + "var", + SyntaxFactory.TriviaList()))) + .WithVariables( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.VariableDeclarator( + SyntaxFactory.Identifier("prefix")) + .WithInitializer( + SyntaxFactory.EqualsValueClause( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(prefix))))))), + SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Debug"), + SyntaxFactory.IdentifierName("Log") + ) + ) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument( + SyntaxFactory.InterpolatedStringExpression( + SyntaxFactory.Token(SyntaxKind.InterpolatedVerbatimStringStartToken) + ) + .WithContents( + SyntaxFactory.List( + new InterpolatedStringContentSyntax[]{ + SyntaxFactory.Interpolation( + SyntaxFactory.IdentifierName("prefix") + ), + SyntaxFactory.InterpolatedStringText() + .WithTextToken( + SyntaxFactory.Token( + SyntaxFactory.TriviaList(), + SyntaxKind.InterpolatedStringTextToken, + " ", + " ", + SyntaxFactory.TriviaList() + ) + ), + SyntaxFactory.Interpolation( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.PredefinedType( + SyntaxFactory.Token(SyntaxKind.StringKeyword) + ), + SyntaxFactory.IdentifierName("Join") + ) + ) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SeparatedList( + new SyntaxNodeOrToken[]{ + SyntaxFactory.Argument( + SyntaxFactory.LiteralExpression( + SyntaxKind.CharacterLiteralExpression, + SyntaxFactory.Literal(';') + ) + ), + SyntaxFactory.Token(SyntaxKind.CommaToken), + SyntaxFactory.Argument( + SyntaxFactory.IdentifierName("source") + ) + } + ) + ) + ) + ) + } + ) + ) + ) + ) + ) + ) + ) + ) + ) + .NormalizeWhitespace(); + } + + public static MethodDeclarationSyntax GenerateSingleLogMethod(string methodName, string text, string prefix) + { + return SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType( + SyntaxFactory.Token(SyntaxKind.VoidKeyword) + ), + SyntaxFactory.Identifier(methodName) + ) + .WithModifiers( + SyntaxFactory.TokenList( + new[]{ + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword) + } + ) + ) + .WithBody( + SyntaxFactory.Block( + SyntaxFactory.SingletonList( + SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Debug"), + SyntaxFactory.IdentifierName("Log") + ) + ) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument( + SyntaxFactory.InterpolatedStringExpression( + SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken) + ) + .WithContents( + SyntaxFactory.List( + new InterpolatedStringContentSyntax[]{ + SyntaxFactory.Interpolation( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(prefix) + ) + ), + SyntaxFactory.InterpolatedStringText() + .WithTextToken( + SyntaxFactory.Token( + SyntaxFactory.TriviaList(), + SyntaxKind.InterpolatedStringTextToken, + " ", + " ", + SyntaxFactory.TriviaList() + ) + ), + SyntaxFactory.Interpolation( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(text) + ) + ) + } + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + .NormalizeWhitespace(); + } + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new ClassDeclarationSyntaxReceiver()); + } + + static string GetTemporaryPath(GeneratorExecutionContext context) + { + string tempPath; + var rootPath = GetProjectRoot(context); + if (rootPath != null) + { + tempPath = Path.Combine(rootPath, "Logs", "Packages", "dev.yarnspinner.unity"); + } + else + { + tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "dev.yarnspinner.logs"); + } + + // we need to make the logs folder, but this can potentially fail + // if it does fail then we will just chuck the logs inside the tmp folder + try + { + if (!Directory.Exists(tempPath)) + { + Directory.CreateDirectory(tempPath); + } + } + catch + { + tempPath = System.IO.Path.GetTempPath(); + } + return tempPath; + } + + public Yarn.Unity.ILogger GetOutput(GeneratorExecutionContext context) + { + if (GetShouldLogToFile(context)) + { + var tempPath = ActionRegistrationSourceGenerator.GetTemporaryPath(context); + + var path = System.IO.Path.Combine(tempPath, $"{nameof(ActionRegistrationSourceGenerator)}-{context.Compilation.AssemblyName}.txt"); + var outFile = System.IO.File.Open(path, System.IO.FileMode.Create); + + return new Yarn.Unity.FileLogger(new System.IO.StreamWriter(outFile)); + } + else + { + return new Yarn.Unity.NullLogger(); + } + } + + private static bool GetShouldLogToFile(GeneratorExecutionContext context) + { + return context.ParseOptions.PreprocessorSymbolNames.Contains(DebugLoggingPreprocessorSymbol); + } + + public void DumpGeneratedFile(GeneratorExecutionContext context, string text) + { + if (GetShouldLogToFile(context)) + { + var tempPath = ActionRegistrationSourceGenerator.GetTemporaryPath(context); + var path = System.IO.Path.Combine(tempPath, $"{nameof(ActionRegistrationSourceGenerator)}-{context.Compilation.AssemblyName}.cs"); + System.IO.File.WriteAllText(path, text); + } + } +} + +internal class ClassDeclarationSyntaxReceiver : ISyntaxReceiver +{ + public List Classes { get; private set; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // Business logic to decide what we're interested in goes here + if (syntaxNode is ClassDeclarationSyntax cds) + { + Classes.Add(cds); + } + } +} + diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionsRegistrationGenerator~/README.md b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionsRegistrationGenerator~/README.md new file mode 100644 index 00000000..3cfb6c5f --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/ActionsRegistrationGenerator~/README.md @@ -0,0 +1,9 @@ +# Actions Registration Generator for Yarn Spinner + +This folder contains the source code for the [source generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) that produces registration code for Yarn Spinner actions (for example, `YarnCommand` and `YarnAction`). + +This folder ends with a tilde `~` to make Unity not aware of it. To build the source code generator, install the .NET SDK and use `dotnet-build`. The built DLL will be placed at the following path: + +``` +(path to Yarn Spinner)/SourceGenerator/YarnSpinner.Unity.SourceCodeGenerator.dll +``` \ No newline at end of file diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/Analyser.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/Analyser.cs new file mode 100644 index 00000000..7fc646a4 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/Analyser.cs @@ -0,0 +1,1165 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using System.Diagnostics.CodeAnalysis; + +#nullable enable + +namespace Yarn.Unity.ActionAnalyser +{ + static class EnumerableExtensions + { + private struct Comparer : IEqualityComparer + { + Func KeyFunc; + public Comparer(Func keyFunc) => this.KeyFunc = keyFunc; + + public readonly bool Equals(TItem x, TItem y) + { + var xKey = KeyFunc(x); + var yKey = KeyFunc(y); + return (xKey == null && yKey == null) || (xKey != null && xKey.Equals(yKey)); + } + + public readonly int GetHashCode(TItem obj) => KeyFunc(obj)?.GetHashCode() ?? 0; + } + public static IEnumerable DistinctBy(this IEnumerable enumerable, Func key) => Enumerable.Distinct(enumerable, new Comparer(key)); + } + + public class Analyser + { + const string toolName = "YarnActionAnalyzer"; + const string toolVersion = "1.0.0.0"; + const string registrationMethodName = "RegisterActions"; + const string targetParameterName = "target"; + const string registrationTypeParameterName = "registrationType"; + const string initialisationMethodName = "AddRegisterFunction"; + + /// + /// The name of a scripting define symbol that, if set, indicates that + /// Yarn actions specific to unit tests should be generated. + /// + public const string GenerateTestActionRegistrationSymbol = "YARN_GENERATE_TEST_ACTION_REGISTRATIONS"; + + public Analyser(string sourcePath) + { + this.SourcePath = sourcePath; + } + + public IEnumerable SourceFiles => GetSourceFiles(SourcePath); + public string SourcePath { get; set; } + + public IEnumerable GetActions(IEnumerable? assemblyPaths = null, ILogger? logger = null) + { + var trees = SourceFiles + .Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), path: path)) + .ToList(); + + var systemAssemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location); + + if (string.IsNullOrEmpty(systemAssemblyPath)) + { + throw new AnalyserException("Unable to find an assembly that defines System.Object"); + } + + static string GetLocationOfAssemblyWithType(string typeName) + { + return GetTypeByName(typeName)?.Assembly.Location ?? throw new AnalyserException($"Failed to find an assembly for type " + typeName); + } + + var runtiem = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + + var netstandard = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "netstandard"); + var systemCore = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.GetName().Name == "System.Core"); + + var references = new List { + MetadataReference.CreateFromFile(netstandard.Location), + MetadataReference.CreateFromFile(systemCore.Location), + MetadataReference.CreateFromFile( + typeof(object).Assembly.Location), + MetadataReference.CreateFromFile( + GetLocationOfAssemblyWithType("Yarn.Unity.DialogueRunner")), + MetadataReference.CreateFromFile( + GetLocationOfAssemblyWithType("Yarn.Unity.YarnCommandAttribute")), + MetadataReference.CreateFromFile( + GetLocationOfAssemblyWithType("UnityEngine.MonoBehaviour")), + }; + + if (assemblyPaths != null) + { + references.AddRange(assemblyPaths.Select(p => MetadataReference.CreateFromFile(p))); + } + + var compilation = CSharpCompilation.Create("YarnActionAnalysis") + .AddReferences(references) + .AddSyntaxTrees(trees); + + var output = new List(); + + var diagnostics = compilation.GetDiagnostics(); + + try + { + foreach (var tree in trees) + { + output.AddRange(GetActions(compilation, tree, logger)); + } + } + catch (Exception e) + { + throw new AnalyserException(e.Message, e, diagnostics); + } + + foreach (var action in output) + { + if (action.Validate(compilation, logger).Any(d => d.Severity == DiagnosticSeverity.Warning || d.Severity == DiagnosticSeverity.Error)) + { + action.ContainsErrors = true; + } + } + + return output; + } + + public static string GenerateRegistrationFileSource(IEnumerable actions, string @namespace = "Yarn.Unity.Generated", string className = "ActionRegistration") + { + var namespaceDecl = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(@namespace)); + + var classDeclaration = SyntaxFactory.ClassDeclaration(className); + classDeclaration = classDeclaration.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); + classDeclaration = classDeclaration.AddAttributeLists(GeneratedCodeAttributeList); + + MethodDeclarationSyntax registrationMethod = GenerateRegistrationMethod(actions); + MethodDeclarationSyntax initializationMethod = GenerateInitialisationMethod(); + + classDeclaration = classDeclaration.AddMembers( + initializationMethod, + registrationMethod + ); + + + namespaceDecl = namespaceDecl.AddMembers(classDeclaration); + + return namespaceDecl.NormalizeWhitespace().ToFullString(); + } + + private static MethodDeclarationSyntax GenerateInitialisationMethod() + { + return SyntaxFactory.MethodDeclaration( + SyntaxFactory.PredefinedType( + SyntaxFactory.Token(SyntaxKind.VoidKeyword) + ), + SyntaxFactory.Identifier(initialisationMethodName) + ) + .WithAttributeLists( + SyntaxFactory.List( + new AttributeListSyntax[]{ + SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Attribute( + SyntaxFactory.QualifiedName( + SyntaxFactory.AliasQualifiedName( + SyntaxFactory.IdentifierName( + SyntaxFactory.Token(SyntaxKind.GlobalKeyword) + ), + SyntaxFactory.IdentifierName("UnityEditor") + ), + SyntaxFactory.IdentifierName("InitializeOnLoadMethod") + ) + ) + ) + ) + .WithOpenBracketToken( + SyntaxFactory.Token( + SyntaxFactory.TriviaList( + SyntaxFactory.Trivia( + SyntaxFactory.IfDirectiveTrivia( + SyntaxFactory.IdentifierName("UNITY_EDITOR"), + true, + true, + true + ) + ) + ), + SyntaxKind.OpenBracketToken, + SyntaxFactory.TriviaList() + ) + ), + SyntaxFactory.AttributeList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Attribute( + SyntaxFactory.QualifiedName( + SyntaxFactory.AliasQualifiedName( + SyntaxFactory.IdentifierName( + SyntaxFactory.Token(SyntaxKind.GlobalKeyword) + ), + SyntaxFactory.IdentifierName("UnityEngine") + ), + SyntaxFactory.IdentifierName("RuntimeInitializeOnLoadMethod") + ) + ) + .WithArgumentList( + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.AttributeArgument( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.AliasQualifiedName( + SyntaxFactory.IdentifierName( + SyntaxFactory.Token(SyntaxKind.GlobalKeyword) + ), + SyntaxFactory.IdentifierName("UnityEngine") + ), + SyntaxFactory.IdentifierName("RuntimeInitializeLoadType") + ), + SyntaxFactory.IdentifierName("BeforeSceneLoad") + ) + ) + ) + ) + ) + ) + ) + .WithOpenBracketToken( + SyntaxFactory.Token( + SyntaxFactory.TriviaList( + SyntaxFactory.Trivia( + SyntaxFactory.EndIfDirectiveTrivia( + true + ) + ) + ), + SyntaxKind.OpenBracketToken, + SyntaxFactory.TriviaList() + ) + ) + } + ) + ) + .WithModifiers( + SyntaxFactory.TokenList( + new[]{ + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword) + } + ) + ) + .WithBody( + SyntaxFactory.Block( + SyntaxFactory.SingletonList( + SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Actions"), + SyntaxFactory.IdentifierName("AddRegistrationMethod") + ) + ) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument( + SyntaxFactory.IdentifierName(registrationMethodName) + ) + ) + ) + ) + ) + ) + ) + ) + .NormalizeWhitespace(); + } + + public static MethodDeclarationSyntax GenerateRegistrationMethod(IEnumerable actions) + { + var actionGroups = actions.GroupBy(a => a.SourceFileName); + + // Create registrations for all attribute registrations + // ([YarnCommand], [YarnFunction]). These registration are always + // invoked. Separately, create registrations for all runtime + // registrations (AddCommandHandler() invocations). These are only + // invoked when the registration methods 'registrationType' + // parameter equals Yarn.Unity.RegistrationType, which is true when + // Yarn Spinner needs to list all commands and functions from + // everywhere. + // + // We should only register runtime-registered commands when + // explicitly requested, because otherwise if we register them here + // AND during gameplay, they'll overlap and cause problems + + var attributeRegistrationStatements = actionGroups.SelectMany(group => + { + + var attributeRegistrations = group + .Where(a => a.DeclarationType != DeclarationType.DirectRegistration); + return GetRegistrationStatements(attributeRegistrations); + }); + + var runtimeRegistrationStatements = actionGroups.SelectMany(group => + { + var runtimeRegistrations = group + .Where(a => a.DeclarationType == DeclarationType.DirectRegistration); + return GetRegistrationStatements(runtimeRegistrations); + }); + + var functionDeclarations = actionGroups.SelectMany(group => + { + var functions = group + .Where(a => a.Type == ActionType.Function); + return GetFunctionDeclarationStatements(functions); + }); + + // Create the method body that combines the attribute-registered and + // (possibly) runtime-registered action registrations. + var registrationMethodBody = SyntaxFactory.Block().WithStatements(SyntaxFactory.List( + attributeRegistrationStatements.Concat(functionDeclarations) + )); + + // Create the list of attributes to attach to this method. + var attributes = SyntaxFactory.List(new[] { GeneratedCodeAttributeList }); + + var methodSyntax = SyntaxFactory.MethodDeclaration( + attributes, // attribute list + SyntaxFactory.TokenList( + SyntaxFactory.Token(SyntaxKind.PublicKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword) + ), // modifiers + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), // return type + null, // explicit interface identifier + SyntaxFactory.Identifier(registrationMethodName), // method name + null, // type parameter list + SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList( + new[] { + SyntaxFactory.Parameter(SyntaxFactory.Identifier(targetParameterName)).WithType(SyntaxFactory.ParseTypeName("global::Yarn.Unity.IActionRegistration")), + SyntaxFactory.Parameter(SyntaxFactory.Identifier(registrationTypeParameterName)).WithType(SyntaxFactory.ParseTypeName("Yarn.Unity.RegistrationType")), + } + )), // parameters + SyntaxFactory.List(), // type parameter constraints + registrationMethodBody, // body + null, // arrow expression clause + SyntaxFactory.Token(SyntaxKind.None) // semicolon token + ); + + return methodSyntax.NormalizeWhitespace(); + + IEnumerable GetRegistrationStatements(IEnumerable registerableCommands) + { + return registerableCommands + .Where(a => a.MethodSymbol?.MethodKind != MethodKind.AnonymousFunction) + .Select((a, i) => + { + var registrationStatement = a.GetRegistrationSyntax(targetParameterName); + if (i == 0) + { + // Add a comment above the first registration that indicates where these actions came from + return registrationStatement.WithLeadingTrivia( + SyntaxFactory.TriviaList( + SyntaxFactory.Comment($"// Actions from file:"), + SyntaxFactory.Comment($"// {a.SourceFileName}") + )); + } + else + { + return registrationStatement; + } + } + ); + } + + IEnumerable GetFunctionDeclarationStatements(IEnumerable functions) + { + return functions + .Select((a, i) => + { + var declarationStatement = a.GetFunctionDeclarationSyntax(targetParameterName); + if (i == 0) + { + // Add a comment above the first registration that indicates where these actions came from + return declarationStatement.WithLeadingTrivia( + SyntaxFactory.TriviaList( + SyntaxFactory.Comment($"// Function declarations from file:"), + SyntaxFactory.Comment($"// {a.SourceFileName}") + )); + } + else + { + return declarationStatement; + } + } + ); + + } + } + + private static AttributeListSyntax GeneratedCodeAttributeList + { + get + { + // [System.CodeDom.Compiler.GeneratedCode(, )] + + var toolNameArgument = SyntaxFactory.AttributeArgument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(toolName) + ) + ); + + var toolVersionArgument = SyntaxFactory.AttributeArgument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(toolVersion) + ) + ); + + return SyntaxFactory.AttributeList( + SyntaxFactory.SeparatedList( + new[] { + SyntaxFactory.Attribute( + SyntaxFactory.ParseName("System.CodeDom.Compiler.GeneratedCode"), + SyntaxFactory.AttributeArgumentList( + SyntaxFactory.SeparatedList( + new[] { + toolNameArgument, + toolVersionArgument, + } + ) + ) + ) + } + ) + ); + } + } + + public static IEnumerable GetActions(CSharpCompilation compilation, SyntaxTree tree, ILogger? yLogger = null) + { + var logger = yLogger; + if (logger == null) + { + logger = new NullLogger(); + } + + var root = tree.GetCompilationUnitRoot(); + + SemanticModel? model = null; + + if (compilation != null) + { + model = compilation.GetSemanticModel(tree, true); + } + + if (model == null) + { + return Array.Empty(); + } + + return GetAttributeActions(root, model, logger).Concat(GetRuntimeDefinedActions(root, model, logger)); + } + + private static IEnumerable GetRuntimeDefinedActions(CompilationUnitSyntax root, SemanticModel model, ILogger? logger) + { + var classes = root.DescendantNodes().OfType(); + classes = classes.Where(c => + { + var classAttributes = GetAttributes(c, model); + + bool hasGeneratedCodeAttribute = classAttributes.Any(attr => + { + var syntax = attr.Item1; + var data = attr.Item2; + + // Check to see if this attribute is the [GeneratedCode] + // attribute + return (data?.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) == "global::System.CodeDom.Compiler.GeneratedCodeAttribute"; + }); + + // Do not visit this class if it is generated code + if (hasGeneratedCodeAttribute) + { + return false; + } + else + { + return true; + } + }); + + var methodInvocations = classes + .SelectMany(classDecl => classDecl.DescendantNodes()) + .OfType() + .Select(i => + { + // Get the symbol represening the method that's being called + var symbolInfo = model.GetSymbolInfo(i.Expression); + + ISymbol? symbol = symbolInfo.Symbol; + + if (symbol == null && symbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure) + { + // We weren't able to determine what specific method + // this was. Pick the first one - we don't actually need + // to know about what specific overload was used, since + // they all have a similar signature. + symbol = symbolInfo.CandidateSymbols.First(); + } + var methodSymbol = symbol as IMethodSymbol; + return (Syntax: i, Symbol: methodSymbol); + }) + .Where(i => i.Symbol != null) + .DistinctBy(i => i.Syntax) + .ToList(); + + var dialogueRunnerCalls = methodInvocations + .Where(info => info.Symbol?.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Yarn.Unity.DialogueRunner").ToList(); + + var addCommandCalls = methodInvocations.Where( + info => info.Symbol?.Name == "AddCommandHandler" + ).Select(c => (Call: c, Type: ActionType.Command)); + + var addFunctionCalls = methodInvocations.Where( + info => info.Symbol?.Name == "AddFunction" + ).Select(c => (Call: c, Type: ActionType.Function)); + + var methodCalls = addCommandCalls.Concat(addFunctionCalls); + + foreach (var methodCall in methodCalls) + { + var (Syntax, Symbol) = methodCall.Call; + + var methodNameSyntax = Syntax.ArgumentList.Arguments.ElementAtOrDefault(0); + var targetSyntax = Syntax.ArgumentList.Arguments.ElementAtOrDefault(1); + if (methodNameSyntax == null || targetSyntax == null) + { + continue; + } + + if (!(model.GetConstantValue(methodNameSyntax.Expression).Value is string name)) + { + // TODO: handle case of 'we couldn't figure out the constant value here' + continue; + } + + SymbolInfo targetSymbolInfo = model.GetSymbolInfo(targetSyntax.Expression); + IMethodSymbol? targetSymbol = targetSymbolInfo.Symbol as IMethodSymbol; + if (targetSymbol == null && targetSymbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure) + { + // We couldn't figure out exactly which of the targets to + // use. Choose one. + targetSymbol = targetSymbolInfo.CandidateSymbols.FirstOrDefault() as IMethodSymbol; + } + if (targetSymbol == null) + { + // TODO: handle case of 'we couldn't figure out target method's + // symbol' + continue; + } + + var declaringSyntax = targetSymbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(); + + string? ReturnDescription = null; + if (TryGetDocumentation(targetSymbol, logger, out XElement? documentationXML, out string? summary)) + { + var returnNode = documentationXML?.Element("returns"); + if (returnNode != null) + { + ReturnDescription = string.Join("", returnNode.DescendantNodes().OfType().Select(n => n.ToString())).Trim(); + logger?.WriteLine($"\tFound a return: {ReturnDescription}"); + } + } + + yield return new Action(name, methodCall.Type, targetSymbol) + { + SemanticModel = model, + MethodName = targetSymbol.Name, + MethodDeclarationSyntax = declaringSyntax, + Declaration = declaringSyntax, + Description = summary, + Parameters = GetParams(targetSymbol, documentationXML, logger), + SourceFileName = root.SyntaxTree.FilePath, + DeclarationType = DeclarationType.DirectRegistration, + ReturnDescription = ReturnDescription, + }; + } + } + + private static bool TryGetDocumentation(IMethodSymbol targetSymbol, ILogger? logger, out XElement? documentationXML, out string? summary) + { + documentationXML = null; + summary = null; + + var documentationComments = targetSymbol.GetDocumentationCommentXml(); + if (string.IsNullOrEmpty(documentationComments)) + { + documentationComments = null; + logger?.WriteLine($"Unable to find any xml documentation for {targetSymbol.Name}, attempting to load it syntactically instead."); + + foreach (var reference in targetSymbol.DeclaringSyntaxReferences) + { + var method = reference.GetSyntax() as MethodDeclarationSyntax; + if (method != null) + { + var comment = GetActionTrivia(method, logger); + if (!string.IsNullOrEmpty(comment)) + { + documentationComments = comment; + break; + } + } + } + } + + // at this point we still don't have a doc string + // going to have to just give up + if (documentationComments == null || string.IsNullOrWhiteSpace(documentationComments)) + { + logger?.WriteLine($"Unable to find any xml documentation for {targetSymbol.Name}, syntactically either."); + return false; + } + logger?.WriteLine($"Found a potential documentation candidate:\"{documentationComments}\""); + + // there are three different situations: + // 1. This is a correctly structured docs string that has come from GetDocumentationCommentXml + if (TryGetXMLFromDocumentString(documentationComments, out documentationXML, logger)) + { + var summaryNode = documentationXML?.Element("summary"); + if (summaryNode != null) + { + summary = string.Join("", summaryNode.DescendantNodes().OfType().Select(n => n.ToString())).Trim(); + logger?.WriteLine("Found the GetDocumentationCommentXml comments and parsed it successfully"); + + return true; + } + } + + // 2. This is a syntactically determined string that happens to also be XML, but it will be missing the synthesised member root + // so we add the missing root node on and try again + if (TryGetXMLFromDocumentString($"{documentationComments}", out documentationXML, logger)) + { + // so we wrap this node and try again + var summaryNode = documentationXML?.Element("summary"); + if (summaryNode != null) + { + summary = string.Join("", summaryNode.DescendantNodes().OfType().Select(n => n.ToString())).Trim(); + logger?.WriteLine("Found the unrooted XML comments and parsed it successfully"); + + return true; + } + } + + // 3. This is not doc XML and just happens to be a comment above a command/function + summary = documentationComments; + documentationXML = null; + logger?.WriteLine("Unable to determine XML, returning the comment as is"); + return true; + } + + private static bool TryGetXMLFromDocumentString(string comment, out XElement? element, ILogger? logger) + { + try + { + element = XElement.Parse(comment); + return true; + } + catch (System.Xml.XmlException ex) + { + logger?.WriteLine("Failed to parse comments as XML"); + logger?.WriteException(ex); + element = null; + return false; + } + } + + private static IEnumerable GetAttributeActions(CompilationUnitSyntax root, SemanticModel model, ILogger logger) + { + var methodInfos = root + .DescendantNodes() + .OfType() + .Where(decl => decl.Parent is ClassDeclarationSyntax); + + var methodsAndSymbols = methodInfos + .Select(decl => + { + return (MethodDeclaration: decl, Symbol: model.GetDeclaredSymbol(decl)); + }) + .Where(pair => pair.Symbol != null); + + var actionMethods = methodsAndSymbols + .Select(pair => + { + var actionType = GetActionType(model, pair.MethodDeclaration, out var attr); + + return (pair.MethodDeclaration, pair.Symbol, ActionType: actionType, ActionAttribute: attr); + }) + .Where(info => info.ActionType != ActionType.NotAnAction); + + foreach (var methodInfo in actionMethods) + { + var attr = methodInfo.ActionAttribute; + + if (attr == null) + { + // Not a valid action; skip + continue; + } + + // working on an assumption that most people just use the method name + string actionName = methodInfo.MethodDeclaration.Identifier.ToString(); + + // handling the situation where they have provided arguments + // if we have an argument list + if (attr.ArgumentList != null) + { + // we resolve the value of first item in that list + // and if it's a string we use that as the action name + var constantValue = model.GetConstantValue(attr.ArgumentList.Arguments.First().Expression); + if (constantValue.HasValue) + { + if (constantValue.Value is string constantString) + { + logger.WriteLine($"resolved constant expression value for the action name: {constantValue.Value.ToString()}"); + actionName = constantString; + } + else + { + // Otherwise just logging the incorrect type and moving on with our life + logger.WriteLine($"resolved constant expression value for the action name, but it is not a string, skipping: {constantValue.Value}"); + } + } + } + + var position = methodInfo.MethodDeclaration.GetLocation(); + var lineIndex = position.GetLineSpan().StartLinePosition.Line + 1; + + var methodSymbol = methodInfo.Symbol; + if (methodSymbol == null) + { + logger.WriteLine($"Failed to get a symbol for " + methodInfo.MethodDeclaration.Identifier); + continue; + } + + if (!(methodSymbol.ContainingSymbol is ITypeSymbol container)) + { + logger.WriteLine($"Failed to get a containing symbol for " + methodInfo.MethodDeclaration.Identifier); + continue; + } + + string? ReturnDescription = null; + if (TryGetDocumentation(methodSymbol, logger, out XElement? documentationXML, out string? summary)) + { + var returnNode = documentationXML?.Element("returns"); + if (returnNode != null) + { + ReturnDescription = string.Join("", returnNode.DescendantNodes().OfType().Select(n => n.ToString())).Trim(); + logger.WriteLine($"\tFound a return: {ReturnDescription}"); + } + } + + var containerName = container?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? ""; + + yield return new Action(actionName, methodInfo.ActionType, methodSymbol) + { + Name = actionName, + Type = methodInfo.ActionType, + MethodName = $"{containerName}.{methodInfo.MethodDeclaration.Identifier}", + MethodIdentifierName = methodInfo.MethodDeclaration.Identifier.ToString(), + MethodSymbol = methodSymbol, + MethodDeclarationSyntax = methodInfo.MethodDeclaration, + IsStatic = methodSymbol.IsStatic, + Declaration = methodInfo.MethodDeclaration, + Parameters = GetParams(methodSymbol, documentationXML, logger), + AsyncType = GetAsyncType(methodSymbol), + SemanticModel = model, + Description = summary, + SourceFileName = root.SyntaxTree.FilePath, + DeclarationType = DeclarationType.Attribute, + ReturnDescription = ReturnDescription, + }; + } + } + + /// + /// Returns a value indicating the Unity async type for this action. + /// + /// The method symbol to test. + /// + private static AsyncType GetAsyncType(IMethodSymbol symbol) + { + var returnType = symbol.ReturnType; + + if (returnType.SpecialType == SpecialType.System_Void) + { + return AsyncType.Sync; + } + + // If the method returns IEnumerator, it is a coroutine, and therefore async. + if (returnType.SpecialType == SpecialType.System_Collections_IEnumerator) + { + return AsyncType.AsyncCoroutine; + } + + // If the method returns a Coroutine, then it is potentially async + // (because if it returns null, it's sync, and if it returns non-null, + // it's async) + if (returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::UnityEngine.Coroutine") + { + return AsyncType.MaybeAsyncCoroutine; + } + + // now checking for the various different awaiter types + // later on it might be worth seeing if there is a good way to check if the return type is something that can be awaited + // but we only have four types so it's probably fine this way + switch (returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) + { + case "global::Yarn.Unity.YarnTask": + case "global::System.Threading.Tasks.Task": + case "global::Cysharp.Threading.Tasks.UniTask": + case "global::UnityEngine.Awaitable": + return AsyncType.AsyncTask; + default: + return default; + }; + } + + private static List GetParams(IMethodSymbol symbol, XElement? documentationXML, ILogger? logger) + { + List parameters = new List(); + + // if this is an instance command registered via the yarn attribute we need to do an extra step + // we will need to create and add in a new parameter to the front of the parameters list + // to represent the game object that we will do a lookup for + var isAttributeRegistered = symbol.GetAttributes().Where(a => a.AttributeClass?.Name == "YarnCommandAttribute").Count() > 0; + if (isAttributeRegistered && !symbol.IsStatic) + { + logger?.WriteLine("Command has been registered via the attribute, will be adding a target parameter."); + var p = new Parameter + { + Name = "target", + IsOptional = false, + Type = symbol.ContainingType, + Description = "The name of the Game Object the runner will search for to run this command upon. This will be done through a normal GameObject.Find Unity call.", + IsParamsArray = false, + }; + parameters.Insert(0, p); + } + + if (symbol.Parameters.Count() > 0) + { + logger?.WriteLine($"Processing {symbol.Name} parameters"); + } + else + { + logger?.WriteLine($"{symbol.Name} has no parameters"); + return parameters; + } + + var parameterDocumentation = new Dictionary(); + + if (documentationXML != null) + { + var parameterNodes = documentationXML.Elements("param"); + foreach (var parameterNode in parameterNodes) + { + var name = parameterNode.Attribute("name"); + if (name == null) { continue; } + var text = string.Join( + "", + parameterNode.DescendantNodes().OfType().Select(v => v.Value) + ).Trim(); + + if (!parameterDocumentation.ContainsKey(name.Value)) + { + parameterDocumentation.Add(name.Value, text); + } + } + } + + foreach (var param in symbol.Parameters) + { + logger?.WriteLine($"\t{param.Name} is a {param.Type.ToDisplayString()}"); + + List attributes = new List(); + foreach (var attribute in param.GetAttributes()) + { + if (attribute.AttributeClass?.BaseType?.Name == "YarnParameterAttribute") + { + logger?.WriteLine($"\t\tattribute: {attribute.AttributeClass?.Name}"); + attributes.Add(attribute); + } + } + + // ok here need to make some changes + // if p is variadic it will be array and I need to get just the T + ITypeSymbol parameterType = param.Type; + if (param.IsParams) + { + if (param.Type is IArrayTypeSymbol arrayTypeSymbol) + { + parameterType = arrayTypeSymbol.ElementType; + } + else + { + logger?.WriteLine($"\t{param.Name} is a variadic parameter but isn't an array"); + } + } + + parameterDocumentation.TryGetValue(param.Name, out var paramDoc); + var p = new Parameter + { + Name = param.Name, + IsOptional = param.IsOptional, + Type = parameterType, + Description = paramDoc, + IsParamsArray = param.IsParams, + Attributes = attributes.Count() == 0 ? null : attributes.ToArray(), + DefaultValueString = param.HasExplicitDefaultValue ? param.ExplicitDefaultValue?.ToString() : null, + }; + parameters.Add(p); + } + + return parameters; + } + + internal static bool IsAttributeYarnCommand(AttributeData attribute) + { + return GetActionType(attribute) != ActionType.NotAnAction; + } + + internal static ActionType GetActionType(SemanticModel model, MethodDeclarationSyntax decl, out AttributeSyntax? actionAttribute) + { + var attributes = GetAttributes(decl, model); + + var actionTypes = attributes + .Select(attr => (Syntax: attr.Item1, Data: attr.Item2, Type: GetActionType(attr.Item2))) + .Where(info => info.Type != ActionType.NotAnAction); + + if (actionTypes.Count() != 1) + { + // Not an action, because you can only have one YarnCommand or + // YarnFunction attribute + actionAttribute = null; + return ActionType.Invalid; + } + + var actionType = actionTypes.Single(); + actionAttribute = actionType.Syntax; + return actionType.Type; + } + + internal static ActionType GetActionType(AttributeData attr) + { + INamedTypeSymbol? attributeClass = attr.AttributeClass; + + switch (attributeClass?.Name) + { + case "YarnCommandAttribute": return ActionType.Command; + case "YarnFunctionAttribute": return ActionType.Function; + default: return ActionType.NotAnAction; + } + } + + public static IEnumerable<(AttributeSyntax, AttributeData)> GetAttributes(ClassDeclarationSyntax classDecl, SemanticModel model) + { + INamedTypeSymbol? classSymbol = model.GetDeclaredSymbol(classDecl); + + var methodAttributes = classSymbol?.GetAttributes(); + + if (methodAttributes == null) + { + yield break; + } + + foreach (var attribute in methodAttributes) + { + if (attribute.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax syntax) + { + yield return (syntax, attribute); + } + } + } + + public static IEnumerable<(AttributeSyntax, AttributeData)> GetAttributes(MethodDeclarationSyntax method, SemanticModel model) + { + IMethodSymbol? methodSymbol = model.GetDeclaredSymbol(method); + + var methodAttributes = methodSymbol?.GetAttributes(); + + if (methodAttributes == null) + { + yield break; + } + + foreach (var attribute in methodAttributes) + { + if (attribute.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax syntax) + { + yield return (syntax, attribute); + } + } + } + + internal static IEnumerable GetSourceFiles(string sourcePath) + { + if (Directory.Exists(sourcePath)) + { + return System.IO.Directory.EnumerateFiles(sourcePath, "*.cs", SearchOption.AllDirectories); + } + if (File.Exists(sourcePath)) + { + return new[] { sourcePath }; + } + throw new FileNotFoundException($"No file or directory at {sourcePath} was found.", sourcePath); + } + + public static Type? GetTypeByName(string name) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + var tt = assembly.GetType(name); + if (tt != null) + { + return tt; + } + } + + return null; + } + + // these are basically just ripped straight from the LSP + // should maybe look at making these more accessible, for now the code dupe is fine IMO + public static string? GetActionTrivia(MethodDeclarationSyntax method, ILogger? logger) + { + // The main string to use as the function's documentation. + if (method.HasLeadingTrivia) + { + var trivias = method.GetLeadingTrivia(); + var structuredTrivia = trivias.LastOrDefault(t => t.HasStructure); + if (structuredTrivia.Kind() != SyntaxKind.None) + { + // The method contains structured trivia. Extract the + // documentation for it. + logger?.WriteLine($"trivia for {method.Identifier} is structured"); + return GetDocumentationFromStructuredTrivia(structuredTrivia); + } + else + { + // There isn't any structured trivia, but perhaps there's a + // comment above the method, which we can use as our + // documentation. + logger?.WriteLine($"trivia for {method.Identifier} is unstructured"); + return GetDocumentationFromUnstructuredTrivia(trivias, logger); + } + } + else + { + logger?.WriteLine($"{method.Identifier} has no trivia"); + return null; + } + } + private static string GetDocumentationFromUnstructuredTrivia(SyntaxTriviaList trivias, ILogger? logger) + { + string documentation; + bool emptyLineFlag = false; + var documentationParts = Enumerable.Empty(); + + // loop in reverse order until hit something that doesn't look like it's related + foreach (var trivia in trivias.Reverse()) + { + var doneWithTrivia = false; + switch (trivia.Kind()) + { + case SyntaxKind.EndOfLineTrivia: + // if we hit two lines in a row without a comment/attribute inbetween, we're done collecting trivia + if (emptyLineFlag == true) + { + logger?.WriteLine("have hit two empty lines in a row, done collecting unstructured trivia"); + doneWithTrivia = true; + } + emptyLineFlag = true; + break; + case SyntaxKind.WhitespaceTrivia: + break; + case SyntaxKind.Attribute: + emptyLineFlag = false; + break; + case SyntaxKind.SingleLineCommentTrivia: + case SyntaxKind.MultiLineCommentTrivia: + documentationParts = documentationParts.Prepend(trivia.ToString().Trim('/', ' ')); + emptyLineFlag = false; + break; + default: + doneWithTrivia = true; + break; + } + + if (doneWithTrivia) + { + break; + } + } + + documentation = string.Join(" ", documentationParts); + return documentation; + } + private static string? GetDocumentationFromStructuredTrivia(SyntaxTrivia structuredTrivia) + { + string documentation; + var triviaStructure = structuredTrivia.GetStructure(); + if (triviaStructure == null) + { + return null; + } + + string? ExtractStructuredTrivia(string tagName) + { + // Find the tag that matches the requested name. + var triviaMatch = triviaStructure + .ChildNodes() + .OfType() + .FirstOrDefault(x => + x.StartTag.Name.ToString() == tagName + ); + + if (triviaMatch != null + && triviaMatch.Kind() != SyntaxKind.None + && triviaMatch.Content.Any()) + { + // Get all content from this element that isn't a newline, and + // join it up into a single string. + var v = triviaMatch + .Content[0] + .ChildTokens() + .Where(ct => ct.Kind() != SyntaxKind.XmlTextLiteralNewLineToken) + .Select(ct => ct.ValueText.Trim()); + + return string.Join(" ", v).Trim(); + } + + return null; + } + + var summary = ExtractStructuredTrivia("summary"); + var remarks = ExtractStructuredTrivia("remarks"); + + documentation = summary ?? triviaStructure.ToString(); + + if (remarks != null) + { + documentation += "\n\n" + remarks; + } + + return documentation; + } + } +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/Analyser.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/Analyser.cs.meta new file mode 100644 index 00000000..804ed024 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/Analyser.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ee966ea0c2700450080c5d533c3c5afc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/AnalyserException.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/AnalyserException.cs new file mode 100644 index 00000000..42f48e29 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/AnalyserException.cs @@ -0,0 +1,25 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using System.Collections.Generic; + +namespace Yarn.Unity.ActionAnalyser +{ + [System.Serializable] + public class AnalyserException : System.Exception + { + public AnalyserException() { } + public AnalyserException(string message) : base(message) { } + public AnalyserException(string message, System.Exception inner) : base(message, inner) { } + public AnalyserException(string message, System.Exception inner, IEnumerable diagnostics) : base(message, inner) + { + this.Diagnostics = diagnostics; + } + protected AnalyserException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + public IEnumerable Diagnostics { get; } = new List(); + } +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/AnalyserException.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/AnalyserException.cs.meta new file mode 100644 index 00000000..5efc4cdf --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/AnalyserException.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4318b63e7e2584e3895f7b9c8782154b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs.meta new file mode 100644 index 00000000..391f5d33 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 38292618b0e0e46e2a23aa0b7ab05be9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.CSharp.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.CSharp.dll new file mode 100644 index 00000000..f1faa593 Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.CSharp.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.CSharp.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.CSharp.dll.meta new file mode 100644 index 00000000..860a5add --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.CSharp.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 4e596e8d36f4949e49a276da7dcf74d0 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.dll new file mode 100644 index 00000000..f6ea89ff Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.dll.meta new file mode 100644 index 00000000..b452c7fe --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/Microsoft.CodeAnalysis.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: d0d5eb061e5a94e61918a4bcc8343d0f +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Buffers.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Buffers.dll new file mode 100644 index 00000000..c0970c07 Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Buffers.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Buffers.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Buffers.dll.meta new file mode 100644 index 00000000..fc6f84a4 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Buffers.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 05b4f6ee4402b4befa8b93e0b0f30072 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Collections.Immutable.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Collections.Immutable.dll new file mode 100644 index 00000000..f737e421 Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Collections.Immutable.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Collections.Immutable.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Collections.Immutable.dll.meta new file mode 100644 index 00000000..6dd74570 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Collections.Immutable.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: d36ed78c475f14eada798a8881d84f4a +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Memory.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Memory.dll new file mode 100644 index 00000000..953a9d2e Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Memory.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Memory.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Memory.dll.meta new file mode 100644 index 00000000..ffcdb3c3 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Memory.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: f124754956f07408dafc0d384a437712 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Reflection.Metadata.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Reflection.Metadata.dll new file mode 100644 index 00000000..5d1f00d3 Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Reflection.Metadata.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Reflection.Metadata.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Reflection.Metadata.dll.meta new file mode 100644 index 00000000..0f644428 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Reflection.Metadata.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 33c2c99bdbd9f48f1bfe7333e3c7f401 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Runtime.CompilerServices.Unsafe.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 00000000..02d98497 Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Runtime.CompilerServices.Unsafe.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Runtime.CompilerServices.Unsafe.dll.meta new file mode 100644 index 00000000..5b2d6535 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Runtime.CompilerServices.Unsafe.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 8807786a5b21e4484ae712f0f92620db +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 1 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Threading.Tasks.Extensions.dll b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Threading.Tasks.Extensions.dll new file mode 100644 index 00000000..dfab2347 Binary files /dev/null and b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Threading.Tasks.Extensions.dll differ diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Threading.Tasks.Extensions.dll.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Threading.Tasks.Extensions.dll.meta new file mode 100644 index 00000000..7f0b929d --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/DLLs/System.Threading.Tasks.Extensions.dll.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 6b1c9dc69df6f47a38bd1ad37d4319bd +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/Diagnostics.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/Diagnostics.cs new file mode 100644 index 00000000..e682e231 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/Diagnostics.cs @@ -0,0 +1,111 @@ +using Microsoft.CodeAnalysis; + + +public static class Diagnostics +{ + public static readonly DiagnosticDescriptor YS1000UnknownError = new DiagnosticDescriptor( + "YS0000", + title: $"Internal unknown error", + messageFormat: "An internal error was encountered while processing this action: {0}", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + public static readonly DiagnosticDescriptor YS1001ActionMethodsMustBePublic = new DiagnosticDescriptor( + "YS1001", + title: $"Yarn action methods must be public", + messageFormat: "YarnCommand and YarnFunction methods must be public. \"{0}\" is {1}.", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "[YarnCommand] and [YarnFunction] attributed methods must be public so that the codegen can reference them.", + helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions"); + + public static readonly DiagnosticDescriptor YS1002ActionMethodsMustHaveAValidName = new DiagnosticDescriptor( + "YS1002", + title: $"Yarn action methods must have a valid name", + messageFormat: "YarnCommand and YarnFunction methods must follow existing ID rules for Yarn. \"{0}\" is invalid.", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "[YarnCommand] and [YarnFunction] attributed methods must follow Yarn ID rules so that Yarn scripts can reference them.", + helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions"); + public static readonly DiagnosticDescriptor YS1003CommandMethodsMustHaveAValidReturnType = new DiagnosticDescriptor( + "YS1003", + title: $"YarnCommand methods must return a valid type", + messageFormat: $"YarnCommand methods must return a valid type (either void, a coroutine, or a task). \"{{0}}\"'s return type is {{1}}.", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions"); + public static readonly DiagnosticDescriptor YS1004FunctionMethodsMustHaveAValidReturnType = new DiagnosticDescriptor( + "YS1004", + title: $"YarnFunction methods must return a valid type", + messageFormat: $"YarnFunction methods must return a valid type (either bool, string, or a numeric type). \"{{0}}\"'s return type is {{1}}.", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions"); + public static readonly DiagnosticDescriptor YS1005ActionMethodsMustHaveOneActionAttribute = new DiagnosticDescriptor( + "YS1005", + title: $"Yarn action methods must have a single YarnCommand or YarnAction attribute", + messageFormat: $"YarnCommand and YarnFunction methods must have a single attribute. \"{{0}}\" has {{1}}.", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions"); + + public static readonly DiagnosticDescriptor YS1006YarnFunctionsMustBeStatic = new DiagnosticDescriptor( + "YS1006", + title: $"YarnFunction methods be static", + messageFormat: $"YarnFunction methods are required to be static.", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions"); + + public static readonly DiagnosticDescriptor YS1008ActionsParamsArraysMustBeOfYarnTypes = new DiagnosticDescriptor( + "YS1008", + title: "Params arrays must be of a Yarn compatible type", + messageFormat: "Params arrays must be of a Yarn compatible type, but {0} is of type \"{1}\"", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions"); + + public static readonly DiagnosticDescriptor YS1009ActionsEnumAttributedParameterIsOfIncompatibleType = new DiagnosticDescriptor( + "YS1009", + title: "Yarn Enum attributed parameters must be of a Yarn compatible type", + messageFormat: "Yarn Enum attributed parameters must be of a Yarn compatible type, but {0} is of type \"{1}\"", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions"); + + public static readonly DiagnosticDescriptor YS1010ActionsNodeAttributedParameterIsOfIncompatibleType = new DiagnosticDescriptor( + "YS1010", + title: "Yarn Node attributed parameters must be a string", + messageFormat: "Yarn Node attributed parameters must be a string, but {0} is of type \"{1}\"", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions"); + + public static readonly DiagnosticDescriptor YS1011ActionsParameterIsAnIncompatibleType = new DiagnosticDescriptor( + "YS1011", + title: "Yarn action parameters must be of a Yarn compatible type", + messageFormat: "Yarn action parameters must be of a Yarn compatible type, but {0} is of type \"{1}\"", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions"); + + public static readonly DiagnosticDescriptor YS1012ActionIsALambda = new DiagnosticDescriptor( + "YS1012", + title: "Yarn actions can be lambdas but this generally isn't recommended", + messageFormat: "Yarn actions can be lambdas but this generally isn't recommended. Lambda based actions cannot be unregistered and are more difficult to debug", + category: "Yarn Spinner", + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + helpLinkUri: "https://docs.yarnspinner.dev/yarn-spinner-for-unity/creating-commands-functions"); + +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/Diagnostics.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/Diagnostics.cs.meta new file mode 100644 index 00000000..4a4e40d4 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/Diagnostics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ef837099f8664a9ab951ed2c6ba6e72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/EnumerableExtensions.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/EnumerableExtensions.cs new file mode 100644 index 00000000..a9a1cdea --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/EnumerableExtensions.cs @@ -0,0 +1,29 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using System; +using System.Collections.Generic; + +#nullable enable + +static class EnumerableExtensions +{ + public static IEnumerable NonNull(this IEnumerable collection, bool throwIfAnyNull = false) where T : class + { + foreach (var item in collection) + { + if (item != null) + { + yield return item; + } + else + { + if (throwIfAnyNull) + { + throw new NullReferenceException("Collection contains a null item"); + } + } + } + } +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/EnumerableExtensions.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/EnumerableExtensions.cs.meta new file mode 100644 index 00000000..45b1b158 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/EnumerableExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a3d39b6f28ae4a31989cb67e5aa3d98 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/ILogger.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/ILogger.cs new file mode 100644 index 00000000..4e3afdac --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/ILogger.cs @@ -0,0 +1,132 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using System; +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +#endif + +namespace Yarn.Unity +{ +#nullable enable + public interface ILogger : IDisposable + { + void Write(object obj); + void WriteLine(object obj); + void WriteException(System.Exception ex, string? message = null); + + void Inc(); + void Dec(); + void SetDepth(int depth); + } + + public class FileLogger : ILogger + { + System.IO.TextWriter writer; + private int depth = 0; + + public FileLogger(System.IO.TextWriter writer) + { + this.writer = writer; + } + + public void Dispose() + { + writer.Flush(); + writer.Dispose(); + } + + public void Write(object text) + { + var tabs = new String('\t', depth); + writer.Write(tabs + text); + } + + public void WriteLine(object text) + { + var tabs = new String('\t', depth); + writer.WriteLine(tabs + text); + } + public void WriteException(System.Exception ex, string? message) + { + var tabs = new String('\t', depth); + if (message == null) + { + writer.WriteLine($"{tabs}Exception: {ex.Message}"); + } + else + { + writer.WriteLine($"{tabs}{message}: {ex.Message}"); + } + } + + public void Inc() + { + depth +=1 ; + } + public void Dec() + { + depth = Math.Max(depth - 1, 0); + } + public void SetDepth(int depth) + { + this.depth = Math.Max(depth, 0); + } + } + + public class UnityLogger : ILogger + { + public void Dispose() { } + + public void Write(object text) + { + WriteLine(text); + } + + public void WriteLine(object text) + { + var tabs = new String('\t', depth); +#if UNITY_EDITOR + Debug.LogWarning(tabs + text.ToString()); +#endif + } + + public void WriteException(System.Exception ex, string? message = null) + { +#if UNITY_EDITOR + Debug.LogException(ex); +#endif + } + + private int depth = 0; + public void Inc() + { + depth +=1 ; + } + public void Dec() + { + depth = Math.Max(depth - 1, 0); + } + public void SetDepth(int depth) + { + this.depth = Math.Max(depth, 0); + } + } + + public class NullLogger : ILogger + { + public void Dispose() { } + + public void Write(object text) { } + + public void WriteLine(object text) { } + + public void WriteException(System.Exception ex, string? message = null) { } + + public void Inc(){} + public void Dec(){} + public void SetDepth(int depth) {} + } +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/ILogger.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/ILogger.cs.meta new file mode 100644 index 00000000..e6c1ea2b --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/ILogger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45e3ce698d3364e5790443887a66daff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/SymbolExtensions.cs b/Packages/dev.yarnspinner.unity/Editor/Analysis/SymbolExtensions.cs new file mode 100644 index 00000000..aa89b995 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/SymbolExtensions.cs @@ -0,0 +1,79 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using Microsoft.CodeAnalysis; +using System.Linq; + +#nullable enable + +static class SymbolExtensions +{ + + /// + /// If the is a method symbol, returns if the method's return type is "awaitable", but not if it's . + /// If the is a type symbol, returns if that type is "awaitable". + /// An "awaitable" is any type that exposes a GetAwaiter method which returns a valid "awaiter". This GetAwaiter method may be an instance method or an extension method. + /// + public static bool IsAwaitableNonDynamic(this ISymbol symbol, SemanticModel semanticModel, int position) + { + IMethodSymbol? methodSymbol = symbol as IMethodSymbol; + ITypeSymbol? typeSymbol = null; + + if (methodSymbol == null) + { + typeSymbol = symbol as ITypeSymbol; + if (typeSymbol == null) + { + return false; + } + } + else + { + if (methodSymbol.ReturnType == null) + { + return false; + } + } + + // otherwise: needs valid GetAwaiter + var potentialGetAwaiters = semanticModel.LookupSymbols(position, + container: typeSymbol ?? methodSymbol?.ReturnType.OriginalDefinition, + name: WellKnownMemberNames.GetAwaiter, + includeReducedExtensionMethods: true); + var getAwaiters = potentialGetAwaiters.OfType().Where(x => !x.Parameters.Any()); + return getAwaiters.Any(VerifyGetAwaiter); + } + + private static bool VerifyGetAwaiter(IMethodSymbol getAwaiter) + { + var returnType = getAwaiter.ReturnType; + if (returnType == null) + { + return false; + } + + // bool IsCompleted { get } + if (!returnType.GetMembers().OfType().Any(p => p.Name == WellKnownMemberNames.IsCompleted && p.Type.SpecialType == SpecialType.System_Boolean && p.GetMethod != null)) + { + return false; + } + + var methods = returnType.GetMembers().OfType(); + + // NOTE: (vladres) The current version of C# Spec, §7.7.7.3 'Runtime evaluation of await expressions', requires that + // NOTE: the interface method INotifyCompletion.OnCompleted or ICriticalNotifyCompletion.UnsafeOnCompleted is invoked + // NOTE: (rather than any OnCompleted method conforming to a certain pattern). + // NOTE: Should this code be updated to match the spec? + + // void OnCompleted(Action) + // Actions are delegates, so we'll just check for delegates. + if (!methods.Any(x => x.Name == WellKnownMemberNames.OnCompleted && x.ReturnsVoid && x.Parameters.Length == 1 && x.Parameters.First().Type.TypeKind == TypeKind.Delegate)) + { + return false; + } + + // void GetResult() || T GetResult() + return methods.Any(m => m.Name == WellKnownMemberNames.GetResult && !m.Parameters.Any()); + } +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Analysis/SymbolExtensions.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Analysis/SymbolExtensions.cs.meta new file mode 100644 index 00000000..7e610a42 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Analysis/SymbolExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 245397f2263474149856dba1cfc50f19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/AssemblyInfo.cs b/Packages/dev.yarnspinner.unity/Editor/AssemblyInfo.cs new file mode 100644 index 00000000..b7f2c6ab --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/AssemblyInfo.cs @@ -0,0 +1,13 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyVersion("3.1.4.0")] +[assembly: AssemblyFileVersion("3.1.4.0")] +[assembly: AssemblyInformationalVersion("3.1.4.Branch.hotfix/textanim-build-errors.Sha.c2b119c5eda7fdd3cd0b13a689f95d54d456fb69")] + +[assembly: InternalsVisibleTo("YarnSpinner.Unity.Editor")] +[assembly: InternalsVisibleTo("YarnSpinner.Unity.Tests.Editor")] diff --git a/Packages/dev.yarnspinner.unity/Editor/AssemblyInfo.cs.meta b/Packages/dev.yarnspinner.unity/Editor/AssemblyInfo.cs.meta new file mode 100644 index 00000000..0d7bafad --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be2fc8d5bc6c7624e95557f8af15a1b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Editors.meta b/Packages/dev.yarnspinner.unity/Editor/Editors.meta new file mode 100644 index 00000000..03cf0b8c --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Editors.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 53fbdca23523543cfbcda84a57c09996 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Editors/AddAssetsToAssetTableCollectionWizard.cs b/Packages/dev.yarnspinner.unity/Editor/Editors/AddAssetsToAssetTableCollectionWizard.cs new file mode 100644 index 00000000..43decd46 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Editors/AddAssetsToAssetTableCollectionWizard.cs @@ -0,0 +1,243 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +#nullable enable + +namespace Yarn.Unity.Editor +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using UnityEditor; + using UnityEditor.AssetImporters; + using UnityEditorInternal; + using UnityEngine; + using Yarn.Compiler; + +#if USE_UNITY_LOCALIZATION + using UnityEditor.Localization; + using UnityEngine.Localization.Tables; + using UnityEngine.Localization; + + public class AddAssetsToAssetTableCollectionWizard : EditorWindow + { + + private YarnProject? yarnProject; + + private AssetTableCollection? assetTableCollection; + + private Type? _assetType; + private Type AssetType + { + get + { + if (_assetType == null) + { + var typeName = EditorPrefs.GetString("YarnSpinner_AddAssets_AssetType", string.Empty); + if (typeName != string.Empty) + { + _assetType = System.Type.GetType(typeName, throwOnError: false, ignoreCase: false); + } + } + _assetType ??= typeof(AudioClip); + return _assetType; + } + set + { + _assetType = value; + EditorPrefs.SetString("YarnSpinner_AddAssets_AssetType", _assetType?.AssemblyQualifiedName ?? string.Empty); + } + } + + private Dictionary localesToFolders = new Dictionary(); + private Type[] allTypes = Array.Empty(); + private GUIContent[] allTypeContents = Array.Empty(); + System.Collections.ObjectModel.ReadOnlyCollection? locales = null; + + private Dictionary _cachedTables = new(); + + const string description = "This tool searches Asset Folders for assets of the selected type, and then adds them to an Asset Table Collection. Line Providers, like Unity Localised Line Provider, can then fetch these assets at run-time."; + + [MenuItem("Window/Yarn Spinner/Add Assets to Table Collection")] + static void Init() + { + // Get existing open window or if none, make a new one: + var window = CreateWindow(); + window.ShowPopup(); + window.titleContent = new GUIContent("Add Assets to Table Collection"); + + if (Selection.activeObject is AssetTableCollection collection) + { + window.assetTableCollection = collection; + } + } + + void OnEnable() + { + allTypes = TypeCache.GetTypesDerivedFrom().OrderBy(t => t.FullName).ToArray(); + allTypeContents = allTypes.Select(t => new GUIContent( + t.FullName.Replace(".", "/"), + EditorGUIUtility.ObjectContent(null, t).image + )).ToArray(); + locales = LocalizationEditorSettings.GetLocales(); + } + + void OnGUI() + { + + using (new GUILayout.VerticalScope()) + { + + EditorGUILayout.BeginVertical(EditorStyles.inspectorFullWidthMargins); + + EditorGUILayout.HelpBox(description, MessageType.Info); + + assetTableCollection = EditorGUILayout.ObjectField(new GUIContent("Asset Table Collection", "The asset table collection to add assets to"), assetTableCollection, typeof(AssetTableCollection), allowSceneObjects: false) as AssetTableCollection; + + + yarnProject = EditorGUILayout.ObjectField(new GUIContent("Yarn Project", "The Yarn Project to add assets for."), yarnProject, typeof(YarnProject), allowSceneObjects: false) as YarnProject; + + var selectedIndex = Array.IndexOf(allTypes, AssetType); + + using (var change = new EditorGUI.ChangeCheckScope()) + { + var newSelection = EditorGUILayout.Popup(new GUIContent("Asset Type", "The type of assets to find in the Asset Folders"), selectedIndex, allTypeContents); + + if (change.changed) + { + AssetType = allTypes[newSelection]; + } + } + + var headerStyle = EditorStyles.boldLabel; + + if (assetTableCollection != null) + { + EditorGUILayout.LabelField("Asset Folders", headerStyle); + + EditorGUI.indentLevel += 1; + + if (locales == null) + { + EditorGUILayout.HelpBox("No locales were found in your project. Is Unity Localization installed and correctly configured?", MessageType.Error); + return; + } + + foreach (var locale in locales) + { + if (_cachedTables.TryGetValue(locale.Identifier, out var localizationTable) == false || localizationTable == null) + { + if (assetTableCollection != null) + { + localizationTable = assetTableCollection.GetTable(locale.Identifier); + + _cachedTables[locale.Identifier] = localizationTable; + } + } + + if (localizationTable == null) + { + EditorGUILayout.LabelField(new GUIContent(locale.LocaleName), new GUIContent("No table in collection")); + continue; + } + + using (new EditorGUI.DisabledGroupScope(localizationTable == null)) + { + localesToFolders.TryGetValue(locale.Identifier.Code, out var currentFolder); + + localesToFolders[locale.Identifier.Code] = EditorGUILayout.ObjectField( + new GUIContent(locale.LocaleName, $"The folder to search for {AssetType.Name} assets for the '{locale.LocaleName}' locale"), + currentFolder, + typeof(DefaultAsset), + allowSceneObjects: false) as DefaultAsset; + } + } + + EditorGUI.indentLevel -= 1; + } + + GUILayout.FlexibleSpace(); + + using (new GUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + + var readyToAddAssets = assetTableCollection != null && yarnProject != null && localesToFolders.Where(kv => kv.Value != null).Count() > 0; + + using (new EditorGUI.DisabledGroupScope(!readyToAddAssets)) + { + if (GUILayout.Button("Add Assets")) + { + AddAssets(assetTableCollection!, yarnProject!, localesToFolders!, AssetType); + // this.Close(); + } + } + } + + EditorGUILayout.EndVertical(); + } + } + + private static void AddAssets(AssetTableCollection assetTableCollection, YarnProject yarnProject, IReadOnlyDictionary localesToFolders, System.Type assetType) + { + var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(yarnProject)) as YarnProjectImporter; + + if (importer == null) + { + throw new InvalidOperationException("Failed to get an importer for Yarn Project"); + } + + var stringTable = importer.GenerateStringsTable(); + + var lineIDs = stringTable.Select(e => e.ID); + + int totalCount = 0; + foreach (var locale in LocalizationEditorSettings.GetLocales()) + { + int perLocaleCount = 0; + if (localesToFolders.TryGetValue(locale.Identifier.Code, out var folder) == false + || folder == null) + { + // No folder given for this locale. Skip it! + Debug.Log($"Skipping {locale.LocaleName} because no folder was provided"); + continue; + } + + if (assetTableCollection.ContainsTable(locale.Identifier) == false) + { + // No table in this collection for this locale! Skip it! + Debug.Log($"Skipping {locale.LocaleName} because no table exists"); + continue; + } + + var path = AssetDatabase.GetAssetPath(folder); + + var idsToAssetPaths = YarnProjectUtility.FindAssetPathsForLineIDs(lineIDs, AssetDatabase.GetAssetPath(folder), assetType); + + foreach (var (lineID, assetPath) in idsToAssetPaths) + { + var asset = AssetDatabase.LoadAssetAtPath(assetPath, assetType); + + if (asset == null) + { + // Not the type of asset we're looking for + continue; + } + + assetTableCollection.AddAssetToTable(locale.Identifier, lineID, asset); + + perLocaleCount += 1; + totalCount += 1; + } + EditorUtility.SetDirty(assetTableCollection); + Debug.Log($"Added {perLocaleCount} assets to {locale.LocaleName}"); + } + Debug.Log($"Added {totalCount} assets to asset table collection"); + + } + } +#endif +} diff --git a/Packages/dev.yarnspinner.unity/Editor/Editors/AddAssetsToAssetTableCollectionWizard.cs.meta b/Packages/dev.yarnspinner.unity/Editor/Editors/AddAssetsToAssetTableCollectionWizard.cs.meta new file mode 100644 index 00000000..a89f03f9 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Editors/AddAssetsToAssetTableCollectionWizard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d2c8a684ed88e47a083d7334e5f24ebc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Editors/Commands.meta b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands.meta new file mode 100644 index 00000000..a06e857d --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6042640492df0421da69d5084456a10c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandElement.uxml b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandElement.uxml new file mode 100644 index 00000000..e6ea9a11 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandElement.uxml @@ -0,0 +1,4 @@ + + + + diff --git a/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandElement.uxml.meta b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandElement.uxml.meta new file mode 100644 index 00000000..8c5bac70 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandElement.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 90cfaddaa17c84dc79cd95f4448553b2 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandsWindow.cs b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandsWindow.cs new file mode 100644 index 00000000..613f1e87 --- /dev/null +++ b/Packages/dev.yarnspinner.unity/Editor/Editors/Commands/CommandsWindow.cs @@ -0,0 +1,195 @@ +/* +Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. +*/ + +#nullable enable + +namespace Yarn.Unity.Editor +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using UnityEditor; + using UnityEngine; + using UnityEngine.UIElements; + using Yarn.Unity; + + internal class CommandsCollection : IActionRegistration + { + public List commandRegistrations = new List(); + + public List<(string Name, Delegate Function)> functionRegistrations = new List<(string Name, Delegate Function)>(); + + public IEnumerable GetListItems() + { + foreach (var registrationMethod in Actions.ActionRegistrationMethods) + { + registrationMethod.Invoke(this, RegistrationType.Compilation); + } + + yield return new CommandsWindow.HeaderListItem("Commands"); + + foreach (var command in commandRegistrations) + { + yield return new CommandsWindow.CommandListItem(command); + } + + // Add a fake 'stop' command to the list, so that it appears in the + // window + System.Action fakeStop = () => { }; + yield return new CommandsWindow.CommandListItem(new Actions.CommandRegistration("stop", fakeStop)); + } + + public void AddCommandHandler(string commandName, Delegate handler) + { + commandRegistrations.Add(new Actions.CommandRegistration(commandName, handler)); + + } + + public void AddCommandHandler(string commandName, MethodInfo methodInfo) + { + commandRegistrations.Add(new Actions.CommandRegistration(commandName, methodInfo)); + } + + public void AddFunction(string name, Delegate implementation) => functionRegistrations.Add((name, implementation)); + + public void RemoveCommandHandler(string commandName) + { + // No-op + } + + public void RemoveFunction(string name) + { + // No-op + } + + public void RegisterFunctionDeclaration(string name, Type returnType, Type[] parameterTypes) + { + /* TODO: Implement */ + } + } + + public class CommandsWindow : EditorWindow + { + public interface IListItem { string DisplayName { get; } } + + public class HeaderListItem : IListItem + { + public string DisplayName { get; set; } + + public HeaderListItem(string displayName) + { + DisplayName = displayName; + } + } + + public class CommandListItem : IListItem + { + internal Actions.CommandRegistration Command; + + internal CommandListItem(Actions.CommandRegistration command) + { + Command = command; + } + + public string DisplayName => Command.Name; + } + + [SerializeField] private VisualTreeAsset? uxml; + [SerializeField] private VisualTreeAsset? listItemUXML; + [SerializeField] private StyleSheet? stylesheet; + + private List listItems = new(); + private List filteredListItems = new(); + + [MenuItem("Window/Yarn Spinner/Commands...")] + static void Summon() + { + var window = GetWindow("Yarn Commands"); + window.Show(); + } + + void CreateGUI() + { + if (uxml == null) + { + Debug.LogWarning($"{GetType()}'s {nameof(uxml)} is null"); + return; + } + uxml.CloneTree(rootVisualElement); + var listView = rootVisualElement.Q(); + + var searchField = rootVisualElement.Q(); + + searchField.RegisterValueChangedCallback(evt => + { + UpdateFilter(listView, searchField.value); + }); + + var commandsCollection = new CommandsCollection(); + + listItems = new List(commandsCollection.GetListItems().OrderBy(item => item.DisplayName)); + + UpdateFilter(listView, searchField.value); + + // Set ListView.makeItem to initialize each entry in the list. + listView.makeItem = () => + { + if (listItemUXML == null) + { + throw new InvalidOperationException($"Can't create new list item: {nameof(listItemUXML)} is null"); + } + + var result = listItemUXML.CloneTree(); + result.styleSheets.Add(stylesheet); + result.AddToClassList("commandListItem"); + return result; + }; + + // Set ListView.bindItem to bind an initialized entry to a data item. + listView.bindItem = (VisualElement element, int index) => + { + var listItem = filteredListItems[index]; + element.Q