diff --git a/Assets/Art/Menu/Common/DifficultyIcons.png b/Assets/Art/Menu/Common/DifficultyIcons.png new file mode 100644 index 0000000000..b46abdea53 --- /dev/null +++ b/Assets/Art/Menu/Common/DifficultyIcons.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbc299d648b9a0d21a5ee3c61e4f349ef58b8c0e6b72afe376cab3b803160e20 +size 475567 diff --git a/Assets/Art/Menu/Common/DifficultyIcons.png.meta b/Assets/Art/Menu/Common/DifficultyIcons.png.meta new file mode 100644 index 0000000000..a529a46bf8 --- /dev/null +++ b/Assets/Art/Menu/Common/DifficultyIcons.png.meta @@ -0,0 +1,221 @@ +fileFormatVersion: 2 +guid: f3ceabe704bcf1745866d8e7135c98ab +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMasterTextureLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 2 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 1 + textureFormat: -1 + textureCompression: 0 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: + - serializedVersion: 2 + name: Easy + rect: + serializedVersion: 2 + x: 512 + y: 512 + width: 512 + height: 512 + alignment: 0 + pivot: {x: 0.5, y: 0.5} + border: {x: 0, y: 0, z: 0, w: 0} + outline: [] + physicsShape: [] + tessellationDetail: 0 + bones: [] + spriteID: af58aba2d1b4ba74182a6307abb7cc73 + internalID: 383874496 + vertices: [] + indices: + edges: [] + weights: [] + - serializedVersion: 2 + name: Medium + rect: + serializedVersion: 2 + x: 1024 + y: 512 + width: 512 + height: 512 + alignment: 0 + pivot: {x: 0.5, y: 0.5} + border: {x: 0, y: 0, z: 0, w: 0} + outline: [] + physicsShape: [] + tessellationDetail: 0 + bones: [] + spriteID: 4b5ee3b91c9ac6d4fbcd4802d9a3a1c5 + internalID: 703388571 + vertices: [] + indices: + edges: [] + weights: [] + - serializedVersion: 2 + name: Hard + rect: + serializedVersion: 2 + x: 1536 + y: 512 + width: 512 + height: 512 + alignment: 0 + pivot: {x: 0.5, y: 0.5} + border: {x: 0, y: 0, z: 0, w: 0} + outline: [] + physicsShape: [] + tessellationDetail: 0 + bones: [] + spriteID: 10ad9e7596ad6684799a88feeee6b20a + internalID: -118138551 + vertices: [] + indices: + edges: [] + weights: [] + - serializedVersion: 2 + name: Expert + rect: + serializedVersion: 2 + x: 0 + y: 0 + width: 512 + height: 512 + alignment: 0 + pivot: {x: 0.5, y: 0.5} + border: {x: 0, y: 0, z: 0, w: 0} + outline: [] + physicsShape: [] + tessellationDetail: 0 + bones: [] + spriteID: ac6616e21c98afe47b0aaf344646cecf + internalID: -1843738572 + vertices: [] + indices: + edges: [] + weights: [] + - serializedVersion: 2 + name: ExpertPlus + rect: + serializedVersion: 2 + x: 512 + y: 0 + width: 512 + height: 512 + alignment: 0 + pivot: {x: 0.5, y: 0.5} + border: {x: 0, y: 0, z: 0, w: 0} + outline: [] + physicsShape: [] + tessellationDetail: 0 + bones: [] + spriteID: e9a2c8c869f9cea4aa29477e3e5f9e8d + internalID: -25976495 + vertices: [] + indices: + edges: [] + weights: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 6cf8484e4acb04c45a7f933ab24af260 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: + Easy: 383874496 + Expert: -1843738572 + ExpertPlus: -25976495 + Hard: -118138551 + Medium: 703388571 + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DifficultySprites.asset b/Assets/DifficultySprites.asset new file mode 100644 index 0000000000..769dca19f0 --- /dev/null +++ b/Assets/DifficultySprites.asset @@ -0,0 +1,433 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &-8242456345906921184 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: TextMeshPro/Sprite + m_Shader: {fileID: 4800000, guid: cf81c85f95fe47e1a27f6ae460cf182c, type: 3} + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _MainTex: + m_Texture: {fileID: 2800000, guid: f3ceabe704bcf1745866d8e7135c98ab, type: 3} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _ColorMask: 15 + - _CullMode: 0 + - _Stencil: 0 + - _StencilComp: 8 + - _StencilOp: 0 + - _StencilReadMask: 255 + - _StencilWriteMask: 255 + - _UseUIAlphaClip: 0 + m_Colors: + - _ClipRect: {r: -32767, g: -32767, b: 32767, a: 32767} + - _Color: {r: 1, g: 1, b: 1, a: 1} + m_BuildTextureStacks: [] +--- !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: 84a92b25f83d49b9bc132d206b370281, type: 3} + m_Name: DifficultySprites + m_EditorClassIdentifier: + hashCode: -1138371127 + material: {fileID: -8242456345906921184} + materialHashCode: 0 + m_Version: 1.1.0 + m_FaceInfo: + m_FaceIndex: 0 + m_FamilyName: + m_StyleName: + m_PointSize: 0 + m_Scale: 0 + m_UnitsPerEM: 0 + m_LineHeight: 0 + m_AscentLine: 0 + m_CapLine: 0 + m_MeanLine: 0 + m_Baseline: 0 + m_DescentLine: 0 + m_SuperscriptOffset: 0 + m_SuperscriptSize: 0 + m_SubscriptOffset: 0 + m_SubscriptSize: 0 + m_UnderlineOffset: 0 + m_UnderlineThickness: 0 + m_StrikethroughOffset: 0 + m_StrikethroughThickness: 0 + m_TabWidth: 0 + spriteSheet: {fileID: 2800000, guid: f3ceabe704bcf1745866d8e7135c98ab, type: 3} + m_SpriteCharacterTable: + - m_ElementType: 2 + m_Unicode: 65534 + m_GlyphIndex: 9 + m_Scale: 1 + m_Name: Easy + m_HashCode: 2377774 + - m_ElementType: 2 + m_Unicode: 65534 + m_GlyphIndex: 10 + m_Scale: 1 + m_Name: Medium + m_HashCode: -1397837123 + - m_ElementType: 2 + m_Unicode: 65534 + m_GlyphIndex: 11 + m_Scale: 1 + m_Name: Hard + m_HashCode: 2553343 + - m_ElementType: 2 + m_Unicode: 65534 + m_GlyphIndex: 12 + m_Scale: 1 + m_Name: Expert + m_HashCode: -1679542706 + - m_ElementType: 2 + m_Unicode: 65534 + m_GlyphIndex: 13 + m_Scale: 1 + m_Name: ExpertPlus + m_HashCode: -994053164 + m_SpriteGlyphTable: + - m_Index: 0 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 0 + m_Y: 1536 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -1834431651, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 1 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 512 + m_Y: 1536 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 1445730593, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 2 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 1024 + m_Y: 1536 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -1591131112, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 3 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 1536 + m_Y: 1536 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -1340302279, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 4 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 0 + m_Y: 1024 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 894865564, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 5 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 512 + m_Y: 1024 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 1760769726, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 6 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 1024 + m_Y: 1024 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -844217933, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 7 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 1536 + m_Y: 1024 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -1173955509, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 8 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 0 + m_Y: 512 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -212029294, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 9 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 512 + m_Y: 512 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 1676829369, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 10 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 1024 + m_Y: 512 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -1041038840, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 11 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 1536 + m_Y: 512 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 1095511443, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 12 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 0 + m_Y: 0 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -413506960, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 13 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 512 + m_Y: 0 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -2104244439, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 14 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: 0 + m_HorizontalBearingY: 375 + m_HorizontalAdvance: 600 + m_GlyphRect: + m_X: 1024 + m_Y: 0 + m_Width: 512 + m_Height: 512 + m_Scale: 1.3 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 1405555635, guid: 66fae0a679753ea4a9110a183b8d2af9, type: 3} + - m_Index: 15 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: -256 + m_HorizontalBearingY: 256 + m_HorizontalAdvance: 512 + m_GlyphRect: + m_X: 512 + m_Y: 512 + m_Width: 512 + m_Height: 512 + m_Scale: 1 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 383874496, guid: f3ceabe704bcf1745866d8e7135c98ab, type: 3} + - m_Index: 16 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: -256 + m_HorizontalBearingY: 256 + m_HorizontalAdvance: 512 + m_GlyphRect: + m_X: 1024 + m_Y: 512 + m_Width: 512 + m_Height: 512 + m_Scale: 1 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: 703388571, guid: f3ceabe704bcf1745866d8e7135c98ab, type: 3} + - m_Index: 17 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: -256 + m_HorizontalBearingY: 256 + m_HorizontalAdvance: 512 + m_GlyphRect: + m_X: 1536 + m_Y: 512 + m_Width: 512 + m_Height: 512 + m_Scale: 1 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -118138551, guid: f3ceabe704bcf1745866d8e7135c98ab, type: 3} + - m_Index: 18 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: -256 + m_HorizontalBearingY: 256 + m_HorizontalAdvance: 512 + m_GlyphRect: + m_X: 0 + m_Y: 0 + m_Width: 512 + m_Height: 512 + m_Scale: 1 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -1843738572, guid: f3ceabe704bcf1745866d8e7135c98ab, type: 3} + - m_Index: 19 + m_Metrics: + m_Width: 512 + m_Height: 512 + m_HorizontalBearingX: -256 + m_HorizontalBearingY: 256 + m_HorizontalAdvance: 512 + m_GlyphRect: + m_X: 512 + m_Y: 0 + m_Width: 512 + m_Height: 512 + m_Scale: 1 + m_AtlasIndex: 0 + m_ClassDefinitionType: 0 + sprite: {fileID: -25976495, guid: f3ceabe704bcf1745866d8e7135c98ab, type: 3} + spriteInfoList: [] + fallbackSpriteAssets: [] diff --git a/Assets/DifficultySprites.asset.meta b/Assets/DifficultySprites.asset.meta new file mode 100644 index 0000000000..edfd809905 --- /dev/null +++ b/Assets/DifficultySprites.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4fe54f44a04e7364db2baafd68cda60c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Menu/MusicLibrary/SongView.prefab b/Assets/Prefabs/Menu/MusicLibrary/SongView.prefab index 0195b9e60c..ff8534f3ca 100644 --- a/Assets/Prefabs/Menu/MusicLibrary/SongView.prefab +++ b/Assets/Prefabs/Menu/MusicLibrary/SongView.prefab @@ -774,7 +774,7 @@ MonoBehaviour: 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_spriteAsset: {fileID: 11400000, guid: 4fe54f44a04e7364db2baafd68cda60c, type: 2} m_tintAllSprites: 0 m_StyleSheet: {fileID: 0} m_TextStyleHashCode: -1183493901 diff --git a/Assets/Script/Gameplay/GameManager.Loading.cs b/Assets/Script/Gameplay/GameManager.Loading.cs index b5cda4504a..94a02c5047 100644 --- a/Assets/Script/Gameplay/GameManager.Loading.cs +++ b/Assets/Script/Gameplay/GameManager.Loading.cs @@ -342,9 +342,9 @@ private void CreatePlayers() player.RefreshPresets(); } - var lastHighScore = ScoreContainer - .GetHighScoreByInstrument(Song.Hash, player.Profile.CurrentInstrument)? - .Score; + var lastHighScore = ScoreContainer.GetHighScore(Song.Hash, player.Profile.Id, player.Profile.CurrentInstrument, false)?.Score; + YargLogger.LogFormatInfo("Current high score for player {0} on {1}: {2}", + player.Profile.Name, player.Profile.CurrentInstrument, lastHighScore ?? 0); if (player.Profile.GameMode != GameMode.Vocals) { diff --git a/Assets/Script/Menu/MusicLibrary/MusicLibraryMenu.cs b/Assets/Script/Menu/MusicLibrary/MusicLibraryMenu.cs index 9d97504ba8..62935fb4a9 100644 --- a/Assets/Script/Menu/MusicLibrary/MusicLibraryMenu.cs +++ b/Assets/Script/Menu/MusicLibrary/MusicLibraryMenu.cs @@ -12,6 +12,7 @@ using YARG.Menu.Navigation; using YARG.Player; using YARG.Playlists; +using YARG.Scores; using YARG.Settings; using YARG.Song; using static YARG.Menu.Navigation.Navigator; @@ -223,6 +224,8 @@ protected override List CreateViewList() return viewList; } + private bool ShouldDisplaySoloHighScores => PlayerContainer.Players.Count(e => !e.Profile.IsBot) == 1; + private List CreateNormalViewList() { var list = new List(); @@ -275,7 +278,7 @@ private List CreateNormalViewList() foreach (var song in _recommendedSongs) { - list.Add(new SongViewType(this, song)); + list.Add(new SongViewType(this, song, GetHighScoreForSong(song), GetBandHighScoreForSong(song))); } _primaryHeaderIndex += _recommendedSongs.Length + 1; } @@ -313,13 +316,43 @@ private List CreateNormalViewList() foreach (var song in section.Songs) { - list.Add(new SongViewType(this, song)); + list.Add(new SongViewType(this, song, GetHighScoreForSong(song), GetBandHighScoreForSong(song))); } } CalculateCategoryHeaderIndices(list); return list; } + private PlayerScoreRecord GetHighScoreForSong(SongEntry song) + { + if (!ShouldDisplaySoloHighScores) + { + return null; + } + + var player = PlayerContainer.Players.First(e => !e.Profile.IsBot); + var result = ScoreContainer.GetHighScore(song.Hash, player.Profile.Id, player.Profile.CurrentInstrument, true); + var percResult = ScoreContainer.GetBestPercentageScore(song.Hash, player.Profile.Id, player.Profile.CurrentInstrument, true); + + if (result != null) + { + Debug.Assert(percResult != null, "Best Percentage score is missing!"); + result.Percent = percResult?.GetPercent() ?? 0; + } + + return result; + } + + private GameRecord GetBandHighScoreForSong(SongEntry song) + { + if (ShouldDisplaySoloHighScores) + { + return null; + } + + return ScoreContainer.GetBandHighScore(song.Hash); + } + private List CreatePlaylistViewList() { var list = new List @@ -339,7 +372,7 @@ private List CreatePlaylistViewList() list.Add(new SortHeaderViewType(section.Category.ToUpperInvariant(), section.Songs.Length, section.CategoryGroup)); foreach (var song in section.Songs) { - list.Add(new SongViewType(this, song)); + list.Add(new SongViewType(this, song, GetHighScoreForSong(song), GetBandHighScoreForSong(song))); } } @@ -387,7 +420,7 @@ private void CalculateCategoryHeaderIndices(List list) private void SetRecommendedSongs() { - if (SongContainer.Count > 5) + if (SongContainer.Count > RecommendedSongs.RECOMMEND_SONGS_COUNT) { _recommendedSongs = RecommendedSongs.GetRecommendedSongs(); } diff --git a/Assets/Script/Menu/MusicLibrary/RecommendedSongs.cs b/Assets/Script/Menu/MusicLibrary/RecommendedSongs.cs index 256b0bc4d8..59fe2a541b 100644 --- a/Assets/Script/Menu/MusicLibrary/RecommendedSongs.cs +++ b/Assets/Script/Menu/MusicLibrary/RecommendedSongs.cs @@ -10,9 +10,11 @@ namespace YARG.Menu.MusicLibrary { public static class RecommendedSongs { + public const int RECOMMEND_SONGS_COUNT = 5; + public static SongEntry[] GetRecommendedSongs() { - var songs = new SongEntry[5]; + var songs = new SongEntry[RECOMMEND_SONGS_COUNT]; int index = 0; AddMostPlayedSongs(songs, ref index); AddRandomSongs(songs, ref index); @@ -45,7 +47,7 @@ private static void AddRandomSongs(SongEntry[] songs, ref int index) SongContainer.Sources.TryGetValue(_YARGSOURCE, out var yargSongs); float yargSongRNG = yargSongs != null ? STARTING_RNG : 0; - while (index < 5) + while (index < RECOMMEND_SONGS_COUNT) { SongEntry song; if (Random.value <= yargSongRNG) diff --git a/Assets/Script/Menu/MusicLibrary/ViewTypes/SongViewType.cs b/Assets/Script/Menu/MusicLibrary/ViewTypes/SongViewType.cs index 1036cbb0c2..e7a4406d5d 100644 --- a/Assets/Script/Menu/MusicLibrary/ViewTypes/SongViewType.cs +++ b/Assets/Script/Menu/MusicLibrary/ViewTypes/SongViewType.cs @@ -1,9 +1,7 @@ using Cysharp.Text; -using Cysharp.Threading.Tasks; using UnityEngine; using YARG.Core.Game; using YARG.Core.Song; -using YARG.Helpers.Extensions; using YARG.Player; using YARG.Playlists; using YARG.Scores; @@ -27,11 +25,15 @@ public class SongViewType : ViewType private readonly MusicLibraryMenu _musicLibrary; public readonly SongEntry SongEntry; + public readonly PlayerScoreRecord PlayerScoreRecord; + public readonly GameRecord BandScoreRecord; - public SongViewType(MusicLibraryMenu musicLibrary, SongEntry songEntry) + public SongViewType(MusicLibraryMenu musicLibrary, SongEntry songEntry, PlayerScoreRecord playerScoreRecord, GameRecord bandScoreRecord) { _musicLibrary = musicLibrary; SongEntry = songEntry; + PlayerScoreRecord = playerScoreRecord; + BandScoreRecord = bandScoreRecord; } public override string GetPrimaryText(bool selected) @@ -53,28 +55,36 @@ public override string GetSecondaryText(bool selected) public override string GetSideText(bool selected) { - var score = ScoreContainer.GetHighScore(SongEntry.Hash); - var bestPctScore = ScoreContainer.GetBestPercentageScore(SongEntry.Hash); + using var builder = ZString.CreateStringBuilder(); + + if (BandScoreRecord != null) + { + // Append the band score if the setting is enabled + if (SettingsManager.Settings.HighScoreInfo.Value == HighScoreInfoMode.Score) + { + builder.AppendFormat(" {0:N0}", BandScoreRecord.BandScore); + } + + return builder.ToString(); + } // Never played! - if (score is null) + if (PlayerScoreRecord is null) { return string.Empty; } - var instrument = score.Instrument.ToResourceName(); - var difficultyChar = score.Difficulty.ToChar(); - var percent = Mathf.Floor(bestPctScore.GetPercent() * 100f); + var difficultySprite = PlayerScoreRecord.Difficulty.ToString(); + var percent = Mathf.Floor(PlayerScoreRecord.GetPercent() * 100f); + var percentColor = PlayerScoreRecord.IsFc ? "#fcd13c" : "#ffffff"; - using var builder = ZString.CreateStringBuilder(); - builder.AppendFormat(" {1} {2:N0}%", - instrument, difficultyChar, percent); + builder.AppendFormat( " {2:N0}%", difficultySprite, percentColor, percent); // Append the score if the setting is enabled if (SettingsManager.Settings.HighScoreInfo.Value == HighScoreInfoMode.Score) { - builder.AppendFormat(" {0:N0}", score.Score); - } + builder.AppendFormat(" {0:N0}", PlayerScoreRecord.Score); + } return builder.ToString(); } @@ -87,8 +97,12 @@ public override string GetSideText(bool selected) return null; } - var score = ScoreContainer.GetHighScore(SongEntry.Hash); - return score?.Stars; + if (BandScoreRecord != null) + { + return BandScoreRecord.BandStars; + } + + return PlayerScoreRecord?.Stars; } public override FavoriteInfo GetFavoriteInfo() @@ -110,7 +124,10 @@ public override void PrimaryButtonClick() { base.PrimaryButtonClick(); - if (PlayerContainer.Players.Count <= 0) return; + if (PlayerContainer.Players.Count <= 0) + { + return; + } GlobalVariables.State.CurrentSong = SongEntry; MenuManager.Instance.PushMenu(MenuManager.Menu.DifficultySelect); diff --git a/Assets/Script/Player/PlayerContainer.cs b/Assets/Script/Player/PlayerContainer.cs index 8449057ea6..4ea4266978 100644 --- a/Assets/Script/Player/PlayerContainer.cs +++ b/Assets/Script/Player/PlayerContainer.cs @@ -100,6 +100,11 @@ public static bool IsProfileTaken(YargProfile profile) return _playersByProfile.ContainsKey(profile); } + public static bool HasBotsActive() + { + return _players.Exists(i => i.Profile.IsBot); + } + public static YargPlayer CreatePlayerFromProfile(YargProfile profile, bool resolveDevices) { if (!_profiles.Contains(profile)) diff --git a/Assets/Script/Scores/ScoreContainer.Queries.cs b/Assets/Script/Scores/ScoreContainer.Queries.cs index 62ba27e552..783d08ac10 100644 --- a/Assets/Script/Scores/ScoreContainer.Queries.cs +++ b/Assets/Script/Scores/ScoreContainer.Queries.cs @@ -8,8 +8,19 @@ public static partial class ScoreContainer private const string QUERY_HIGH_SCORES = @" SELECT *, MAX(Score) FROM PlayerScores INNER JOIN GameRecords ON PlayerScores.GameRecordId = GameRecords.Id + WHERE PlayerId = ? AND Instrument = ? GROUP BY GameRecords.SongChecksum"; + private const string QUERY_SINGLE_PLAYER_HIGH_SCORE = @" + SELECT * + FROM PlayerScores + INNER JOIN GameRecords ON PlayerScores.GameRecordId = GameRecords.Id + WHERE GameRecords.SongChecksum = ? + AND PlayerScores.Instrument = ? + AND PlayerScores.PlayerId = ? + ORDER BY Score DESC + LIMIT 1"; + private const string QUERY_UPDATE_NULL_PERCENTS = @" UPDATE PlayerScores SET Percent = cast(NotesHit as REAL) / (NotesHit + NotesMissed) @@ -20,18 +31,27 @@ UPDATE PlayerScores /* For each song, this retrieves the records with the best score */ BestScore as (SELECT *, max(Score) FROM PlayerScores INNER JOIN GameRecords ON PlayerScores.GameRecordId = GameRecords.Id - GROUP BY GameRecords.SongChecksum), + GROUP BY GameRecords.SongChecksum, PlayerScores.Instrument, PlayerScores.PlayerId), /* For each song, instrument, and difficulty, this retrieves the records with the best score by percent */ BestPercents as (SELECT *, max(Percent) FROM PlayerScores INNER JOIN GameRecords ON PlayerScores.GameRecordId = GameRecords.Id - GROUP BY GameRecords.SongChecksum, PlayerScores.Instrument, PlayerScores.Difficulty) + GROUP BY GameRecords.SongChecksum, PlayerScores.Instrument, PlayerScores.PlayerId, PlayerScores.Difficulty) /* Filter the lines from the BestPercents temporary table above where the instrument and difficulty match the instrument and difficulty when the highest score was recorded */ SELECT BestPercents.* - FROM BestPercents INNER JOIN BestScore ON BestScore.Instrument = BestPercents.Instrument + FROM BestPercents + INNER JOIN BestScore ON BestScore.Instrument = BestPercents.Instrument AND BestScore.Difficulty = BestPercents.Difficulty - AND BestScore.SongChecksum = BestPercents.SongChecksum"; + AND BestScore.SongChecksum = BestPercents.SongChecksum + AND BestScore.PlayerId = BestPercents.PlayerId + WHERE BestScore.PlayerId = ? AND BestScore.Instrument = ?; + "; + + private const string QUERY_BAND_BEST_SCORES = @" + SELECT *, MAX(BandScore) FROM GameRecords + GROUP BY GameRecords.SongChecksum + "; } } \ No newline at end of file diff --git a/Assets/Script/Scores/ScoreContainer.cs b/Assets/Script/Scores/ScoreContainer.cs index 02b86399f5..05a4398741 100644 --- a/Assets/Script/Scores/ScoreContainer.cs +++ b/Assets/Script/Scores/ScoreContainer.cs @@ -15,15 +15,19 @@ namespace YARG.Scores { public static partial class ScoreContainer { - public static string ScoreDirectory { get; private set; } + public static string ScoreDirectory { get; private set; } public static string ScoreReplayDirectory { get; private set; } private static string _scoreDatabaseFile; private static SQLiteConnection _db; - private static readonly Dictionary SongHighScores = new(); - private static readonly Dictionary SongHighScoresByPct = new(); + private static readonly Dictionary PlayerHighScores = new(); + private static readonly Dictionary PlayerHighScoresByPct = new(); + private static readonly Dictionary BandHighScores = new(); + + private static Instrument _currentInstrument = Instrument.Band; + private static Guid _currentPlayerId; public static void Init() { @@ -41,7 +45,7 @@ public static void Init() _db = new SQLiteConnection(_scoreDatabaseFile); InitDatabase(); UpdateNullPercents(); - FetchHighScores(); + FetchBandHighScores(); } catch (Exception e) { @@ -49,6 +53,24 @@ public static void Init() } } + private static void FetchBandHighScores() + { + try + { + BandHighScores.Clear(); + var result = _db.Query(QUERY_BAND_BEST_SCORES); + + foreach (var record in result) + { + BandHighScores.Add(HashWrapper.Create(record.SongChecksum), record); + } + } + catch (Exception e) + { + YargLogger.LogException(e, "Failed to load all GameRecords from database."); + } + } + private static void InitDatabase() { _db.CreateTable(); @@ -108,34 +130,14 @@ public static void RecordScore(GameRecord gameRecord, List pl var songChecksum = HashWrapper.Create(gameRecord.SongChecksum); - var bestScore = playerEntries.Find(p => p.Score == playerEntries.Max(x => x.Score)); - - if (SongHighScores.TryGetValue(songChecksum, out var highScore)) - { - if (bestScore.Score > highScore.Score) - { - SongHighScores[songChecksum] = bestScore; - - if (bestScore.Instrument != highScore.Instrument || bestScore.Difficulty != highScore.Difficulty) - { - SongHighScoresByPct[songChecksum] = bestScore; - } - } - - if (bestScore.Instrument == highScore.Instrument && bestScore.Difficulty == highScore.Difficulty) - { - if (bestScore.GetPercent() > highScore.GetPercent()) - { - SongHighScoresByPct[songChecksum] = bestScore; - } - } - } - else + if (playerEntries.Count == 1) { - SongHighScores.Add(songChecksum, bestScore); - SongHighScoresByPct.Add(songChecksum, bestScore); + // Update the cached player high scores. Only relevant if there is a single human player. + UpdatePlayerHighScores(songChecksum, playerEntries.First()); } + UpdateBandHighScore(songChecksum, gameRecord); + YargLogger.LogInfo("Recorded high score for song."); } catch (Exception e) @@ -144,6 +146,49 @@ public static void RecordScore(GameRecord gameRecord, List pl } } + private static void UpdateBandHighScore(HashWrapper songChecksum, GameRecord currentBandScore) + { + if (!BandHighScores.TryGetValue(songChecksum, out var currentBest)) + { + BandHighScores.Add(songChecksum, currentBandScore); + return; + } + if (currentBest.BandScore >= currentBandScore.BandScore) + { + return; + } + + BandHighScores[songChecksum] = currentBandScore; + } + + private static void UpdatePlayerHighScores(HashWrapper songChecksum, PlayerScoreRecord newScore) + { + PlayerHighScoresByPct.TryGetValue(songChecksum, out var currentBestPct); + if (!PlayerHighScores.TryGetValue(songChecksum, out var currentBest)) + { + PlayerHighScores.Add(songChecksum, newScore); + PlayerHighScoresByPct.Add(songChecksum, newScore); + return; + } + + if (newScore.Score > currentBest.Score) + { + PlayerHighScores[songChecksum] = newScore; + + // If there's a new best score on a different difficulty, the old best percentage entry is irrelevant. Use the new score as the new best percentage entry. + if (newScore.Difficulty != currentBest.Difficulty || newScore.GetPercent() > currentBest.GetPercent()) + { + PlayerHighScoresByPct[songChecksum] = newScore; + } + } + + // Update the best percentage entry if appropriate. + if (newScore.Difficulty == currentBestPct.Difficulty || newScore.GetPercent() > currentBest.GetPercent()) + { + PlayerHighScoresByPct[songChecksum] = newScore; + } + } + public static void RecordPlayerInfo(Guid id, string name) { var query = $"SELECT * FROM Players WHERE Id = '{id}'"; @@ -192,14 +237,81 @@ public static List GetAllPlayerScores(Guid id) return null; } - public static PlayerScoreRecord GetHighScore(HashWrapper songChecksum) + /// + /// Get the highest score (in points) for a specific song, player and instrument. + /// If `allowCacheUpdate` is true, all high scores for the player and instrument will be fetched from the database and cached, to speed up subsequent player high score requests. + /// + /// The ID of the song to retrieve a high score for. + /// The ID of the player profile to retrieve a high score for. + /// The instrument to retrieve a high score for. + /// Sets whether all high scores for this player and instrument should be cached. Set this to true when fetching a large number of high scores for the same player and instrument + /// (such as when displaying high scores on the Music Library). Set this to false when fetching multiple high scores for different players in a row. + /// The highest score for the provided song, player and instrument, or null if no high score exists. + public static PlayerScoreRecord GetHighScore(HashWrapper songChecksum, Guid playerId, Instrument instrument, bool allowCacheUpdate = true) { - return SongHighScores?.GetValueOrDefault(songChecksum); + if (allowCacheUpdate) + { + FetchHighScores(playerId, instrument); + } + + if (_currentInstrument == instrument && _currentPlayerId == playerId) + { + return PlayerHighScores.GetValueOrDefault(songChecksum); + } + + return GetHighScoreFromDatabase(songChecksum, playerId, instrument); + } + + public static GameRecord GetBandHighScore(HashWrapper songChecksum) + { + if (BandHighScores.TryGetValue(songChecksum, out var record)) + { + return record; + } + + return null; + } + + private static PlayerScoreRecord GetHighScoreFromDatabase(HashWrapper songChecksum, Guid playerId, Instrument instrument) + { + var query = QUERY_SINGLE_PLAYER_HIGH_SCORE; + + try + { + return _db.FindWithQuery(query, songChecksum.ToString(), playerId, (int) instrument); + } + catch (Exception e) + { + YargLogger.LogException(e, $"Failed to load high score from database for player with ID {playerId}."); + return null; + } } - public static PlayerScoreRecord GetBestPercentageScore(HashWrapper songChecksum) + /// + /// Get the highest score percentage (regardless of points) for a specific song, player and instrument. + /// Note that this can be different from the highest score in points, as it is possible to get a higher percentage with a lower score. + /// If `allowCacheUpdate` is true, all high scores for the player and instrument will be fetched from the database and cached, to speed up subsequent player high score requests. + /// + /// The ID of the song to retrieve a high score for. + /// The ID of the player profile to retrieve a high score for. + /// The instrument to retrieve a high score for. + /// Sets whether all high scores for this player and instrument should be cached. Set this to true when fetching a large number of high scores for the same player and instrument + /// (such as when displaying high scores on the Music Library). Set this to false when fetching multiple high scores for different players in a row. + /// The highest score percentage for the provided song, player and instrument, or null if no high score exists. + public static PlayerScoreRecord GetBestPercentageScore(HashWrapper songChecksum, Guid playerId, Instrument instrument, bool allowCacheUpdate = true) { - return SongHighScoresByPct?.GetValueOrDefault(songChecksum); + if (allowCacheUpdate) + { + FetchHighScores(playerId, instrument); + } + + if (_currentInstrument == instrument && _currentPlayerId == playerId) + { + return PlayerHighScoresByPct.GetValueOrDefault(songChecksum); + } + + // TODO: Fetch best % from database if there's a cache miss. Not currently used. + throw new NotImplementedException("Attempted to load best solo score percentage from database. Not implemented."); } public static void UpdateNullPercents() @@ -218,12 +330,22 @@ public static void UpdateNullPercents() } } - public static void FetchHighScores() + + private static void FetchHighScores(Guid playerId, Instrument instrument) { + if (_currentPlayerId == playerId && _currentInstrument == instrument && PlayerHighScores.Any()) + { + // Already cached. No need to fetch again from the database. + return; + } + try { - var scoreResults = _db.Query(QUERY_HIGH_SCORES); - var songResults = _db.Query(QUERY_SONGS); + PlayerHighScores.Clear(); + PlayerHighScoresByPct.Clear(); + + var scoreResults = _db.Query(QUERY_HIGH_SCORES, playerId, (int) instrument); + var songResults = _db.Query(QUERY_SONGS, playerId, (int) instrument); foreach (var score in scoreResults) { var song = songResults.FirstOrDefault(x => x.Id == score.GameRecordId); @@ -232,10 +354,10 @@ public static void FetchHighScores() continue; } - SongHighScores.Add(HashWrapper.Create(song.SongChecksum), score); + PlayerHighScores.Add(HashWrapper.Create(song.SongChecksum), score); } - var scoreResultsByPct = _db.Query(QUERY_BEST_SCORES_BY_PERCENT); + var scoreResultsByPct = _db.Query(QUERY_BEST_SCORES_BY_PERCENT, playerId, (int) instrument); foreach (var score in scoreResultsByPct) { var song = songResults.FirstOrDefault(x => x.Id == score.GameRecordId); @@ -244,31 +366,16 @@ public static void FetchHighScores() continue; } - SongHighScoresByPct.Add(HashWrapper.Create(song.SongChecksum), score); + PlayerHighScoresByPct.Add(HashWrapper.Create(song.SongChecksum), score); } - } - catch (Exception e) - { - YargLogger.LogException(e, "Failed to load high score from database."); - } - } - public static PlayerScoreRecord GetHighScoreByInstrument(HashWrapper songChecksum, Instrument instrument) - { - try - { - var query = - $"SELECT * FROM PlayerScores INNER JOIN GameRecords ON PlayerScores.GameRecordId = GameRecords.Id WHERE " + - $"GameRecords.SongChecksum = x'{songChecksum.ToString()}' AND PlayerScores.Instrument = {(int) instrument} " + - $"ORDER BY Score DESC LIMIT 1"; - return _db.FindWithQuery(query); + _currentInstrument = instrument; + _currentPlayerId = playerId; } catch (Exception e) { YargLogger.LogException(e, "Failed to load high score from database."); } - - return null; } public static List GetMostPlayedSongs(int maxCount) diff --git a/Assets/Settings/AddressableAssetsData/AssetGroups/Default Local Group.asset b/Assets/Settings/AddressableAssetsData/AssetGroups/Default Local Group.asset index 1f46c38ca9..433c0f3fc9 100644 --- a/Assets/Settings/AddressableAssetsData/AssetGroups/Default Local Group.asset +++ b/Assets/Settings/AddressableAssetsData/AssetGroups/Default Local Group.asset @@ -212,6 +212,11 @@ MonoBehaviour: m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: f3ceabe704bcf1745866d8e7135c98ab + m_Address: DifficultyIcons + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 0 m_Settings: {fileID: 11400000, guid: e05274e5aee7ff54bb550557e8a635f2, type: 2} m_SchemaSet: diff --git a/Assets/StreamingAssets/lang/en-US.json b/Assets/StreamingAssets/lang/en-US.json index 54f1c2c4e2..652bac76e4 100644 --- a/Assets/StreamingAssets/lang/en-US.json +++ b/Assets/StreamingAssets/lang/en-US.json @@ -288,7 +288,7 @@ "Players" : "Players" }, "ScoreScreen" : { - "BandScoreNotSaved" : "(Band score not saved)" + "BandScoreNotSaved" : "(Score not saved)" }, "Settings": { "NoFolder": "No Folder",