diff --git a/Assets/Resources/Prefabs/Global/Globals.prefab b/Assets/Resources/Prefabs/Global/Globals.prefab index 8e27430..b351018 100644 --- a/Assets/Resources/Prefabs/Global/Globals.prefab +++ b/Assets/Resources/Prefabs/Global/Globals.prefab @@ -11,7 +11,6 @@ GameObject: - component: {fileID: 6708844088101150} - component: {fileID: -6830167632558745604} - component: {fileID: 7290507550622501242} - - component: {fileID: 4577859410310799261} - component: {fileID: -3401184227323537348} - component: {fileID: -6245296037342915518} - component: {fileID: 6566017136146807207} @@ -66,30 +65,6 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Settings.SettingsManager database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2} ---- !u!114 &4577859410310799261 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 4763496076470024150} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d793a5f3669a676418d43519feaf3a8d, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Controls.ComWrapper - saveMoneyBetweenSessions: 0 - enablePrizeChecking: 0 - keepAlive: 1 - enableEncryptedHandshake: 1 - handshakeTimeoutMs: 5000 - roleRequestCommand: 97 - roleResponsePrefix: ROLE - roleRequestTimeoutMs: 1500 - mainControllerRole: main_controller - useAllDetectedPorts: 1 - explicitPorts: [] - defaultDevicePort: --- !u!114 &-3401184227323537348 MonoBehaviour: m_ObjectHideFlags: 0 @@ -110,10 +85,17 @@ MonoBehaviour: roleRequestCommand: 97 roleResponsePrefix: ROLE roleRequestTimeoutMs: 1500 - mainControllerRole: main_controller + mainControllerRole: main useAllDetectedPorts: 1 explicitPorts: [] defaultDevicePort: + roleHandlerBindings: + - role: prize_stand + handlerType: 1 + prizeStandBoxesCount: 24 + - role: prize_stand_controller + handlerType: 1 + prizeStandBoxesCount: 24 --- !u!114 &-6245296037342915518 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Scenes/99-99.unity b/Assets/Scenes/99-99.unity index 7f9e936..8bea116 100644 --- a/Assets/Scenes/99-99.unity +++ b/Assets/Scenes/99-99.unity @@ -8504,6 +8504,59 @@ Transform: m_CorrespondingSourceObject: {fileID: 4000011826580284, guid: 93b90907e99e38a448aca4707806a9be, type: 3} m_PrefabInstance: {fileID: 1487759007} m_PrefabAsset: {fileID: 0} +--- !u!1 &1500633490 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1500633492} + - component: {fileID: 1500633491} + m_Layer: 0 + m_Name: Handshake_Test + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!114 &1500633491 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1500633490} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 50f614532ed49204cb6644534a1c5d81, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Handshake_Test + portName: COM3 + baudRate: 115200 + readTimeoutMs: 5 + writeTimeoutMs: 10 + openOnStart: 1 + autoRunHandshake: 1 + handshakeTimeoutMs: 10000 + closePortOnDisable: 1 + logRawReadLoop: 1 +--- !u!4 &1500633492 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1500633490} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -2.75464, y: -3.18366, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1513556535 GameObject: m_ObjectHideFlags: 0 @@ -289291,3 +289344,4 @@ SceneRoots: - {fileID: 2033907137} - {fileID: 1553660625} - {fileID: 348531057} + - {fileID: 1500633492} diff --git a/Assets/Scenes/MainMenu.unity b/Assets/Scenes/MainMenu.unity index d6d3194..35de794 100644 --- a/Assets/Scenes/MainMenu.unity +++ b/Assets/Scenes/MainMenu.unity @@ -3929,7 +3929,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1669870415} - - {fileID: 1962581740} m_Father: {fileID: 139708007} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -4351,7 +4350,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1877012750} - - {fileID: 1906929718} m_Father: {fileID: 139708007} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -4945,42 +4943,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 683903126} m_CullTransparentMesh: 1 ---- !u!1 &700047461 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 700047462} - m_Layer: 5 - m_Name: VFX - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 0 ---- !u!224 &700047462 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 700047461} - 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: 997740367} - m_Father: {fileID: 1460883867} - 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: 100, y: 100} - m_Pivot: {x: 0.5, y: 0.5} --- !u!1 &705422857 GameObject: m_ObjectHideFlags: 0 @@ -7459,124 +7421,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 996229917} m_CullTransparentMesh: 1 ---- !u!1 &997740366 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 997740367} - - component: {fileID: 997740369} - - component: {fileID: 997740368} - m_Layer: 5 - m_Name: Visual Effect - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &997740367 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 997740366} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 100, y: 100, z: 100} - m_ConstrainProportionsScale: 1 - m_Children: [] - m_Father: {fileID: 700047462} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!73398921 &997740368 -VFXRenderer: - serializedVersion: 1 - m_ObjectHideFlags: 2 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 997740366} - m_Enabled: 0 - m_CastShadows: 0 - m_ReceiveShadows: 0 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 0 - m_LightProbeUsage: 0 - m_ReflectionProbeUsage: 0 - m_RayTracingMode: 0 - m_RayTraceProcedural: 0 - m_RayTracingAccelStructBuildFlagsOverride: 0 - m_RayTracingAccelStructBuildFlags: 1 - m_SmallMeshCulling: 1 - m_ForceMeshLod: -1 - m_MeshLodSelectionBias: 0 - m_RenderingLayerMask: 4294967295 - m_RendererPriority: 0 - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 3 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 981438795 - m_SortingLayer: 2 - m_SortingOrder: 0 - m_MaskInteraction: 0 ---- !u!2083052967 &997740369 -VisualEffect: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 997740366} - m_Enabled: 1 - m_Asset: {fileID: 8926484042661614526, guid: 6c4fdce0f9a1b184d9c3660014bdfdb5, type: 3} - m_InitialEventName: OnPlay - m_InitialEventNameOverriden: 0 - m_StartSeed: 0 - m_ResetSeedOnPlay: 1 - m_AllowInstancing: 1 - m_ResourceVersion: 1 - m_PropertySheet: - m_Float: - m_Array: [] - m_Vector2f: - m_Array: [] - m_Vector3f: - m_Array: [] - m_Vector4f: - m_Array: [] - m_Uint: - m_Array: [] - m_Int: - m_Array: [] - m_Matrix4x4f: - m_Array: [] - m_AnimationCurve: - m_Array: [] - m_Gradient: - m_Array: [] - m_NamedObject: - m_Array: [] - m_Bool: - m_Array: [] --- !u!1 &1010062389 GameObject: m_ObjectHideFlags: 0 @@ -9936,8 +9780,8 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_Sprite: {fileID: -5002131913879957837, guid: 67ae0b8315a808542a646c540231aa88, type: 3} - m_Type: 0 + m_Sprite: {fileID: 21300000, guid: 67ae0b8315a808542a646c540231aa88, type: 3} + m_Type: 1 m_PreserveAspect: 0 m_FillCenter: 1 m_FillMethod: 4 @@ -10384,7 +10228,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1412406034} - - {fileID: 700047462} m_Father: {fileID: 139708007} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -13553,7 +13396,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0.00094176567} + m_AnchoredPosition: {x: 0, y: 0.00095310435} m_SizeDelta: {x: 0, y: 300} m_Pivot: {x: 0, y: 1} --- !u!114 &1792218764 @@ -13925,7 +13768,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 374987852} - - {fileID: 2141805163} m_Father: {fileID: 139708007} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -14825,74 +14667,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1906471957} m_CullTransparentMesh: 1 ---- !u!1 &1906929717 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1906929718} - - component: {fileID: 1906929719} - m_Layer: 5 - m_Name: SettingsUIGenerator - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1906929718 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1906929717} - 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: 650017796} - 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: 100, y: 100} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1906929719 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1906929717} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator - database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2} - contentRoot: {fileID: 0} - groupByGroup: 1 - collapsibleGroups: 1 - groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3} - defaultGroupsExpanded: 1 - rememberGroupFoldout: 1 - groupOrder: - - "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438" - sortByNameInsideGroup: 0 - toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3} - sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3} - valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3} - dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3} - inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3} - buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3} - optionPickerRowPrefab: {fileID: 0} - buildOnEnable: 1 - clearBeforeBuild: 1 - showDescription: 1 --- !u!1 &1925001478 GameObject: m_ObjectHideFlags: 0 @@ -15184,74 +14958,6 @@ MonoBehaviour: m_StringArgument: m_BoolArgument: 0 m_CallState: 2 ---- !u!1 &1935615349 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1935615350} - - component: {fileID: 1935615351} - m_Layer: 5 - m_Name: SettingsUIGenerator - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1935615350 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1935615349} - 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: 2009866677} - 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: 100, y: 100} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1935615351 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1935615349} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator - database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2} - contentRoot: {fileID: 0} - groupByGroup: 1 - collapsibleGroups: 1 - groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3} - defaultGroupsExpanded: 1 - rememberGroupFoldout: 1 - groupOrder: - - "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438" - sortByNameInsideGroup: 0 - toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3} - sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3} - valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3} - dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3} - inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3} - buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3} - optionPickerRowPrefab: {fileID: 0} - buildOnEnable: 1 - clearBeforeBuild: 1 - showDescription: 1 --- !u!1 &1959311158 GameObject: m_ObjectHideFlags: 0 @@ -15422,74 +15128,6 @@ Transform: - {fileID: 1594047442} m_Father: {fileID: 1381629685} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &1962581739 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1962581740} - - component: {fileID: 1962581741} - m_Layer: 5 - m_Name: SettingsUIGenerator - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &1962581740 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1962581739} - 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: 554903410} - 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: 100, y: 100} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &1962581741 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1962581739} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator - database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2} - contentRoot: {fileID: 0} - groupByGroup: 1 - collapsibleGroups: 1 - groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3} - defaultGroupsExpanded: 1 - rememberGroupFoldout: 1 - groupOrder: - - "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438" - sortByNameInsideGroup: 0 - toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3} - sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3} - valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3} - dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3} - inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3} - buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3} - optionPickerRowPrefab: {fileID: 0} - buildOnEnable: 1 - clearBeforeBuild: 1 - showDescription: 1 --- !u!1 &1986812974 GameObject: m_ObjectHideFlags: 0 @@ -15860,7 +15498,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 765378070} - - {fileID: 1935615350} m_Father: {fileID: 139708007} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -16798,74 +16435,6 @@ MonoBehaviour: m_FlexibleWidth: -1 m_FlexibleHeight: -1 m_LayoutPriority: 1 ---- !u!1 &2141805162 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 2141805163} - - component: {fileID: 2141805164} - m_Layer: 5 - m_Name: SettingsUIGenerator - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!224 &2141805163 -RectTransform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2141805162} - 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: 1828951630} - 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: 100, y: 100} - m_Pivot: {x: 0.5, y: 0.5} ---- !u!114 &2141805164 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2141805162} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator - database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2} - contentRoot: {fileID: 0} - groupByGroup: 1 - collapsibleGroups: 1 - groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3} - defaultGroupsExpanded: 1 - rememberGroupFoldout: 1 - groupOrder: - - "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438" - sortByNameInsideGroup: 0 - toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3} - sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3} - valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3} - dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3} - inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3} - buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3} - optionPickerRowPrefab: {fileID: 0} - buildOnEnable: 1 - clearBeforeBuild: 1 - showDescription: 1 --- !u!1 &1000009598396499 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/ComDeviceClient.cs b/Assets/Scripts/Controls/Inputs/ComSystem/ComDeviceClient.cs index 4bbdb45..2fa91c5 100644 --- a/Assets/Scripts/Controls/Inputs/ComSystem/ComDeviceClient.cs +++ b/Assets/Scripts/Controls/Inputs/ComSystem/ComDeviceClient.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using RR.Game2D.Core.Managers; +using UnityEngine; namespace RR.Common.Controls { @@ -37,25 +38,27 @@ namespace RR.Common.Controls private int _handle; private long _lastCoinTime; private int _inputToggle; + private int _missedInputResponses; private bool _isAuthorized; private bool _isOperational; private bool _handshakeInProgress; private string _requestHex = string.Empty; + private string _lastHandshakeResponse = string.Empty; private string _lastInputBits = DefaultInputBits; private byte[] _aesKey32; private HandshakeStage _handshakeStage = HandshakeStage.None; private TaskCompletionSource _handshakeCompletion; + private TaskCompletionSource _chalReceivedCompletion; private TaskCompletionSource _roleCompletion; private enum HandshakeStage { None, - WaitingRaes, WaitingChal, - WaitingReady, + WaitingRaes, Completed, Failed } @@ -69,6 +72,7 @@ namespace RR.Common.Controls public event Action CoinAccepted; public event Action InputReceived; + public event Action RawLineReceived; public event Action Authorized; public event Action Disconnected; @@ -146,8 +150,10 @@ namespace RR.Common.Controls _handshakeInProgress = false; _handshakeStage = HandshakeStage.None; _handshakeCompletion?.TrySetCanceled(); + _chalReceivedCompletion?.TrySetCanceled(); _roleCompletion?.TrySetCanceled(); ControllerRole = string.Empty; + _keepAliveTask = null; Disconnected?.Invoke(DeviceId); @@ -230,7 +236,10 @@ namespace RR.Common.Controls string bits = DecimalToBinary(rawData); _lastInputBits = bits; InputReceived?.Invoke(DeviceId, bits); + return; } + + RawLineReceived?.Invoke(DeviceId, line); } private async Task RunHandshakeAsync(CancellationToken token) @@ -238,59 +247,88 @@ namespace RR.Common.Controls _handshakeInProgress = true; _isAuthorized = false; _isOperational = false; - _handshakeStage = HandshakeStage.WaitingRaes; + _handshakeStage = HandshakeStage.WaitingChal; + _lastHandshakeResponse = string.Empty; _handshakeCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - SendRaw("00", true); - await Task.Delay(1000, token); + _chalReceivedCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Debug.Log($"[COM AUTH][{DeviceId}] Handshake started. Stage={_handshakeStage}"); _requestHex = GenerateRandomBlock16Hex(); + // Порядок handshake по протоколу платы: + // PC -> 07[random], board -> CHAL, PC -> 08[enc(CHAL)], board -> RAES, board -> AUTH_CNT_OK. SendRaw("07" + _requestHex, true); + Debug.Log($"[COM AUTH][{DeviceId}] -> 07{_requestHex}"); + + // Если на 07 не получили ожидаемый CHAL быстро, сразу пропускаем устройство. + int chalFastTimeoutMs = Math.Min(1200, Math.Max(250, _handshakeTimeoutMs / 4)); + Task chalFastTimeoutTask = Task.Delay(chalFastTimeoutMs, token); + Task chalFastFinished = await Task.WhenAny(_chalReceivedCompletion.Task, chalFastTimeoutTask); + if (chalFastFinished != _chalReceivedCompletion.Task) + { + Debug.LogWarning($"[COM AUTH][{DeviceId}] No CHAL after 07 in {chalFastTimeoutMs}ms. Skipping device."); + FailHandshake($"Нет ожидаемого CHAL на 07 за {chalFastTimeoutMs}ms"); + return; + } Task timeoutTask = Task.Delay(_handshakeTimeoutMs, token); Task finished = await Task.WhenAny(_handshakeCompletion.Task, timeoutTask); if (finished == _handshakeCompletion.Task && _handshakeCompletion.Task.Result) { + Debug.Log($"[COM AUTH][{DeviceId}] Handshake completed successfully"); MarkOperational(); return; } - FailHandshake("Таймаут рукопожатия"); + Debug.LogWarning($"[COM AUTH][{DeviceId}] Handshake timeout. Last board response: {_lastHandshakeResponse}"); + FailHandshake($"Таймаут рукопожатия. Последний ответ платы: {_lastHandshakeResponse}"); } private void TryProcessHandshakeLine(string line) { - if (line.Equals("READY", StringComparison.OrdinalIgnoreCase)) + Debug.Log($"[COM AUTH][{DeviceId}] <- {line}; stage={_handshakeStage}"); + _lastHandshakeResponse = line; + + // Во время handshake обрабатываем только ожидаемые сообщения: + // WaitingChal -> CHAL + // WaitingRaes -> RAES + // Все остальные строки игнорируем. + if (_handshakeStage == HandshakeStage.WaitingChal) { - if (_handshakeStage != HandshakeStage.WaitingReady) + if (line.StartsWith("CHAL", StringComparison.Ordinal)) { - FailHandshake($"READY в неверной фазе: {_handshakeStage}"); - return; + HandleChal(line); + } + else + { + Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingChal: {line}"); } - _handshakeStage = HandshakeStage.Completed; - _isAuthorized = true; - _handshakeCompletion?.TrySetResult(true); return; } - if (line.StartsWith("RAES", StringComparison.Ordinal)) + if (_handshakeStage == HandshakeStage.WaitingRaes) { - HandleRaes(line); + if (line.StartsWith("RAES", StringComparison.Ordinal)) + { + HandleRaes(line); + } + else + { + Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingRaes: {line}"); + } + return; } - if (line.StartsWith("CHAL", StringComparison.Ordinal)) - { - HandleChal(line); - } + Debug.Log($"[COM AUTH][{DeviceId}] Ignored in stage {_handshakeStage}: {line}"); } private void HandleRaes(string line) { try { + Debug.Log($"[COM AUTH][{DeviceId}] Handling RAES..."); if (_handshakeStage != HandshakeStage.WaitingRaes) { FailHandshake($"RAES в неверной фазе: {_handshakeStage}"); @@ -317,7 +355,10 @@ namespace RR.Common.Controls return; } - _handshakeStage = HandshakeStage.WaitingChal; + _handshakeStage = HandshakeStage.Completed; + _isAuthorized = true; + Debug.Log($"[COM AUTH][{DeviceId}] RAES verified. Raw RAES={line}; decrypted={decrypted}; handshake complete"); + _handshakeCompletion?.TrySetResult(true); } catch (Exception e) { @@ -329,12 +370,15 @@ namespace RR.Common.Controls { try { + Debug.Log($"[COM AUTH][{DeviceId}] Handling CHAL..."); if (_handshakeStage != HandshakeStage.WaitingChal) { FailHandshake($"CHAL в неверной фазе: {_handshakeStage}"); return; } + _chalReceivedCompletion?.TrySetResult(true); + if (line.Length < 36) { FailHandshake("Некорректная длина CHAL"); @@ -350,7 +394,8 @@ namespace RR.Common.Controls string encrypted = EncryptHexBlock16(hex); SendRaw("08" + encrypted, true); - _handshakeStage = HandshakeStage.WaitingReady; + _handshakeStage = HandshakeStage.WaitingRaes; + Debug.Log($"[COM AUTH][{DeviceId}] -> 08{encrypted}; stage={_handshakeStage}"); } catch (Exception e) { @@ -365,10 +410,12 @@ namespace RR.Common.Controls _isOperational = true; _handshakeStage = HandshakeStage.Completed; ControllerRole = string.Empty; + _missedInputResponses = 0; + Debug.Log($"[COM AUTH][{DeviceId}] Device marked operational"); SendRaw("2171", true); - if (_keepAlive) + if (_keepAlive && (_keepAliveTask == null || _keepAliveTask.IsCompleted)) { _keepAliveTask = Task.Run(() => KeepAliveLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token); } @@ -383,7 +430,12 @@ namespace RR.Common.Controls _handshakeInProgress = false; _handshakeStage = HandshakeStage.Failed; _handshakeCompletion?.TrySetResult(false); - GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] {message}"); + Debug.LogWarning($"[COM AUTH][{DeviceId}] Handshake failed: {message}"); + GameManager.Logger?.LogWarning("ComDeviceClient", $"[{DeviceId}] {message}"); + + // Если порт отвечает не по ожидаемому протоколу платы, + // отключаем устройство, чтобы менеджер выбрал другой порт. + Stop(false); } private async Task KeepAliveLoopAsync(CancellationToken token) @@ -406,7 +458,18 @@ namespace RR.Common.Controls await Task.Delay(1000, token); if (_inputToggle == 0) { - await ReconnectAsync(token); + _missedInputResponses++; + if (_missedInputResponses >= 3) + { + GameManager.Logger?.LogWarning("ComDeviceClient", + $"[{DeviceId}] Нет INPUT-ответа {_missedInputResponses} цикла подряд, выполняю reconnect"); + _missedInputResponses = 0; + await ReconnectAsync(token); + } + } + else + { + _missedInputResponses = 0; } await Task.Delay(7000, token); @@ -443,6 +506,9 @@ namespace RR.Common.Controls if (!OpenPort()) { + // Порт недоступен при переподключении: отключаем устройство и даем менеджеру + // переключиться на другие доступные порты. + Stop(false); return; } diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/ComMultiManager.cs b/Assets/Scripts/Controls/Inputs/ComSystem/ComHub.cs similarity index 64% rename from Assets/Scripts/Controls/Inputs/ComSystem/ComMultiManager.cs rename to Assets/Scripts/Controls/Inputs/ComSystem/ComHub.cs index 6f14445..5db138a 100644 --- a/Assets/Scripts/Controls/Inputs/ComSystem/ComMultiManager.cs +++ b/Assets/Scripts/Controls/Inputs/ComSystem/ComHub.cs @@ -6,9 +6,10 @@ using RR.Game2D.Core.Managers; namespace RR.Common.Controls { - internal sealed class ComMultiManager + internal sealed class ComHub { private readonly Dictionary _devices = new(); + private readonly Dictionary _handlers = new(); private readonly ConcurrentQueue _coinQueue = new(); private readonly ConcurrentQueue _inputQueue = new(); private readonly ConcurrentQueue _authorizedQueue = new(); @@ -23,10 +24,11 @@ namespace RR.Common.Controls private readonly string _roleResponsePrefix; private readonly int _roleRequestTimeoutMs; private readonly string _mainControllerRole; + private readonly Func _handlerFactory; private string _defaultDeviceId = string.Empty; - public ComMultiManager( + public ComHub( bool useAllDetectedPorts, string[] explicitPorts, string defaultDevicePort, @@ -36,7 +38,8 @@ namespace RR.Common.Controls string roleRequestCommand, string roleResponsePrefix, int roleRequestTimeoutMs, - string mainControllerRole) + string mainControllerRole, + Func handlerFactory) { _useAllDetectedPorts = useAllDetectedPorts; _explicitPorts = explicitPorts ?? Array.Empty(); @@ -48,6 +51,7 @@ namespace RR.Common.Controls _roleResponsePrefix = roleResponsePrefix ?? "ROLE"; _roleRequestTimeoutMs = roleRequestTimeoutMs; _mainControllerRole = string.IsNullOrWhiteSpace(mainControllerRole) ? "main_controller" : mainControllerRole; + _handlerFactory = handlerFactory; } public string DefaultDeviceId => _defaultDeviceId; @@ -61,7 +65,7 @@ namespace RR.Common.Controls string[] allPorts = ComNativeWrapper.ListPorts(); if (allPorts.Length == 0) { - GameManager.Logger?.LogWarning("ComMultiManager", "Не найдено COM-портов"); + GameManager.Logger?.LogWarning("ComHub", "Не найдено COM-портов"); return; } @@ -82,24 +86,50 @@ namespace RR.Common.Controls client.Stop(sendShutdownCommands); } + foreach (IComControllerHandler handler in _handlers.Values) + { + handler.Dispose(); + } + _devices.Clear(); + _handlers.Clear(); _defaultDeviceId = string.Empty; - while (_coinQueue.TryDequeue(out _)) { } - while (_inputQueue.TryDequeue(out _)) { } - while (_authorizedQueue.TryDequeue(out _)) { } + while (_coinQueue.TryDequeue(out _)) + { + } + + while (_inputQueue.TryDequeue(out _)) + { + } + + while (_authorizedQueue.TryDequeue(out _)) + { + } } public bool SendToDefault(string message) { - if (string.IsNullOrEmpty(_defaultDeviceId)) return false; + if (string.IsNullOrEmpty(_defaultDeviceId)) + { + return false; + } + return SendToDevice(_defaultDeviceId, message); } public bool SendToDevice(string deviceId, string message) { - if (string.IsNullOrWhiteSpace(deviceId)) return false; - if (!_devices.TryGetValue(deviceId, out ComDeviceClient client)) return false; + if (string.IsNullOrWhiteSpace(deviceId)) + { + return false; + } + + if (!_devices.TryGetValue(deviceId, out ComDeviceClient client)) + { + return false; + } + client.Send(message); return true; } @@ -114,7 +144,11 @@ namespace RR.Common.Controls public string GetLastInputState(string deviceId) { - if (string.IsNullOrWhiteSpace(deviceId)) return "11111111111111111111111111"; + if (string.IsNullOrWhiteSpace(deviceId)) + { + return "11111111111111111111111111"; + } + return _devices.TryGetValue(deviceId, out ComDeviceClient client) ? client.LastInputBits : "11111111111111111111111111"; @@ -128,18 +162,49 @@ namespace RR.Common.Controls public string GetDeviceRole(string deviceId) { - if (string.IsNullOrWhiteSpace(deviceId)) return string.Empty; + if (string.IsNullOrWhiteSpace(deviceId)) + { + return string.Empty; + } + return _devices.TryGetValue(deviceId, out ComDeviceClient client) ? client.ControllerRole : string.Empty; } + public PrizeStandController GetPrizeStandController(string deviceId = null) + { + if (!string.IsNullOrWhiteSpace(deviceId)) + { + return _handlers.TryGetValue(deviceId, out IComControllerHandler handler) + ? handler as PrizeStandController + : null; + } + + foreach (IComControllerHandler handler in _handlers.Values) + { + if (handler is PrizeStandController standController) + { + return standController; + } + } + + return null; + } + private string[] SelectPorts(string[] allPorts) { - if (_useAllDetectedPorts) return allPorts; - if (_explicitPorts.Length == 0) return Array.Empty(); + if (_useAllDetectedPorts) + { + return allPorts; + } - HashSet explicitSet = new HashSet(_explicitPorts.Where(p => !string.IsNullOrWhiteSpace(p))); + if (_explicitPorts.Length == 0) + { + return Array.Empty(); + } + + HashSet explicitSet = new(_explicitPorts.Where(p => !string.IsNullOrWhiteSpace(p))); return allPorts.Where(explicitSet.Contains).ToArray(); } @@ -156,7 +221,7 @@ namespace RR.Common.Controls private void TryCreateDevice(string port) { - ComDeviceClient client = new ComDeviceClient( + ComDeviceClient client = new( port, _enableEncryptedHandshake, _handshakeTimeoutMs, @@ -180,6 +245,7 @@ namespace RR.Common.Controls { client.CoinAccepted += OnDeviceCoinAccepted; client.InputReceived += OnDeviceInputReceived; + client.RawLineReceived += OnDeviceRawLineReceived; client.Authorized += OnDeviceAuthorized; client.Disconnected += OnDeviceDisconnected; } @@ -188,20 +254,41 @@ namespace RR.Common.Controls { client.CoinAccepted -= OnDeviceCoinAccepted; client.InputReceived -= OnDeviceInputReceived; + client.RawLineReceived -= OnDeviceRawLineReceived; client.Authorized -= OnDeviceAuthorized; client.Disconnected -= OnDeviceDisconnected; } private void OnDeviceCoinAccepted(string deviceId) { + if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler)) + { + handler.HandleCoinAccepted(); + return; + } + _coinQueue.Enqueue(deviceId); } private void OnDeviceInputReceived(string deviceId, string inputBits) { + if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler)) + { + handler.HandleInputReceived(inputBits); + return; + } + _inputQueue.Enqueue(new DeviceInputMessage(deviceId, inputBits)); } + private void OnDeviceRawLineReceived(string deviceId, string line) + { + if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler)) + { + handler.HandleRawLine(line); + } + } + private void OnDeviceAuthorized(string deviceId) { if (_devices.TryGetValue(deviceId, out ComDeviceClient client) && @@ -214,20 +301,48 @@ namespace RR.Common.Controls _defaultDeviceId = deviceId; } + if (_handlers.TryGetValue(deviceId, out IComControllerHandler existing)) + { + existing.Dispose(); + } + + _handlers[deviceId] = CreateHandlerForDevice(deviceId); _authorizedQueue.Enqueue(deviceId); } private void OnDeviceDisconnected(string deviceId) { - if (!_devices.TryGetValue(deviceId, out ComDeviceClient client)) return; + if (!_devices.TryGetValue(deviceId, out ComDeviceClient client)) + { + return; + } + UnsubscribeDevice(client); _devices.Remove(deviceId); + if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler)) + { + handler.Dispose(); + _handlers.Remove(deviceId); + } + if (_defaultDeviceId == deviceId) { _defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty; } } + + private IComControllerHandler CreateHandlerForDevice(string deviceId) + { + string role = GetDeviceRole(deviceId); + IComControllerHandler customHandler = _handlerFactory?.Invoke(deviceId, role); + if (customHandler != null) + { + return customHandler; + } + + return new DefaultComControllerHandler(deviceId, _coinQueue, _inputQueue); + } } internal readonly struct DeviceInputMessage @@ -242,3 +357,5 @@ namespace RR.Common.Controls } } } + + diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/ComMultiManager.cs.meta b/Assets/Scripts/Controls/Inputs/ComSystem/ComHub.cs.meta similarity index 100% rename from Assets/Scripts/Controls/Inputs/ComSystem/ComMultiManager.cs.meta rename to Assets/Scripts/Controls/Inputs/ComSystem/ComHub.cs.meta diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/ComSystemManager.cs b/Assets/Scripts/Controls/Inputs/ComSystem/ComSystemManager.cs index b05150c..182ef68 100644 --- a/Assets/Scripts/Controls/Inputs/ComSystem/ComSystemManager.cs +++ b/Assets/Scripts/Controls/Inputs/ComSystem/ComSystemManager.cs @@ -1,13 +1,305 @@ +using System; +using RR.Game2D.Core.Managers; using UnityEngine; +using UnityEngine.Events; namespace RR.Common.Controls { /// - /// Новый публичный компонент COM-системы. - /// Наследует реализацию от для плавной миграции. + /// Публичный компонент COM-системы. /// [AddComponentMenu("RR/Controls/Com System Manager")] - public class ComSystemManager : ComWrapper + public class ComSystemManager : MonoBehaviour { + private enum ControllerHandlerType + { + Default = 0, + PrizeStand = 1 + } + + [Serializable] + private sealed class RoleHandlerBinding + { + public string role; + public ControllerHandlerType handlerType = ControllerHandlerType.Default; + [Min(0)] public int prizeStandBoxesCount; + } + + [Header("Основные настройки")] + [SerializeField] private bool saveMoneyBetweenSessions; + [SerializeField] private bool enablePrizeChecking; + [SerializeField] private bool keepAlive = true; + + [Header("Авторизация")] + [SerializeField] private bool enableEncryptedHandshake = true; + [SerializeField] private int handshakeTimeoutMs = 5000; + + [Header("Роль контроллера (после handshake)")] + [SerializeField] private string roleRequestCommand = "97"; + [SerializeField] private string roleResponsePrefix = "ROLE"; + [SerializeField] private int roleRequestTimeoutMs = 1500; + [SerializeField] private string mainControllerRole = "main_controller"; + + [Header("Мульти-устройства")] + [SerializeField] private bool useAllDetectedPorts = true; + [SerializeField] private string[] explicitPorts = Array.Empty(); + [SerializeField] private string defaultDevicePort = ""; + + [Header("Фабрика обработчиков по ROLE")] + [SerializeField] private RoleHandlerBinding[] roleHandlerBindings = + { + new RoleHandlerBinding { role = "prize_stand", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 }, + new RoleHandlerBinding { role = "prize_stand_controller", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 } + }; + + public static UnityEvent OnCreditAdded; + public static UnityEvent OnCreditRemoved; + public static UnityEvent OnInputReceived; + public static Action OnInitialize; + + public static UnityEvent OnDeviceInputReceived; + public static UnityEvent OnDeviceAuthorized; + + private static ComSystemManager _instance; + private static int _creditsCount; + + private ComHub _manager; + private LightController _light; + private bool _initializeInvoked; + + public LightController Light => _light; + + public void Initialize() + { + _instance = this; + + OnCreditAdded = new UnityEvent(); + OnCreditRemoved = new UnityEvent(); + OnInputReceived = new UnityEvent(); + OnDeviceInputReceived = new UnityEvent(); + OnDeviceAuthorized = new UnityEvent(); + + if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0); + _creditsCount = PlayerPrefs.GetInt("CoinsCount", 0); + + _light = GetComponent() ?? gameObject.AddComponent(); + + _manager = new ComHub( + useAllDetectedPorts, + explicitPorts, + defaultDevicePort, + enableEncryptedHandshake, + handshakeTimeoutMs, + keepAlive || enablePrizeChecking, + roleRequestCommand, + roleResponsePrefix, + roleRequestTimeoutMs, + mainControllerRole, + CreateRoleHandler); + + _manager.InitializeDevices(); + } + + public static void AddCredit() + { + _creditsCount++; + PlayerPrefs.SetInt("CoinsCount", _creditsCount); + OnCreditAdded?.Invoke(_creditsCount); + } + + public static bool Pay(int amount = 1) + { + if (_creditsCount < amount) + { + return false; + } + + _creditsCount -= amount; + PlayerPrefs.SetInt("CoinsCount", _creditsCount); + + PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1); + PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1); + + OnCreditRemoved?.Invoke(_creditsCount); + return true; + } + + public static int GetCredits() => _creditsCount; + + public static void SetCredits(int amount) + { + int oldCredits = _creditsCount; + _creditsCount = Mathf.Max(0, amount); + PlayerPrefs.SetInt("CoinsCount", _creditsCount); + + if (_creditsCount > oldCredits) + { + OnCreditAdded?.Invoke(_creditsCount); + } + else if (_creditsCount < oldCredits) + { + OnCreditRemoved?.Invoke(_creditsCount); + } + } + + public static void Send(string message) + { + if (_instance == null || string.IsNullOrWhiteSpace(message)) return; + _instance._manager?.SendToDefault(message); + } + + public static void SendToDevice(string deviceId, string message) + { + if (_instance == null || string.IsNullOrWhiteSpace(message)) return; + _instance._manager?.SendToDevice(deviceId, message); + } + + public static void SendToAll(string message) + { + if (_instance == null || string.IsNullOrWhiteSpace(message)) return; + _instance._manager?.SendToAll(message); + } + + public static string GetLastInputState() + { + if (_instance?._manager == null) return "11111111111111111111111111"; + string defaultId = _instance._manager.DefaultDeviceId; + return string.IsNullOrWhiteSpace(defaultId) + ? "11111111111111111111111111" + : _instance._manager.GetLastInputState(defaultId); + } + + public static string GetLastInputState(string deviceId) + { + return _instance?._manager?.GetLastInputState(deviceId) ?? "11111111111111111111111111"; + } + + public static string[] GetConnectedDeviceIds() + { + return _instance?._manager?.ConnectedDeviceIds ?? Array.Empty(); + } + + public static string GetDefaultDeviceId() + { + return _instance?._manager?.DefaultDeviceId ?? string.Empty; + } + + public static string GetDeviceRole(string deviceId) + { + return _instance?._manager?.GetDeviceRole(deviceId) ?? string.Empty; + } + + public static PrizeStandController GetPrizeStandController(string deviceId = null) + { + return _instance?._manager?.GetPrizeStandController(deviceId); + } + + private IComControllerHandler CreateRoleHandler(string deviceId, string role) + { + if (string.IsNullOrWhiteSpace(role) || roleHandlerBindings == null || roleHandlerBindings.Length == 0) + { + return null; + } + + RoleHandlerBinding binding = FindRoleBinding(role); + if (binding == null) + { + return null; + } + + if (binding.handlerType == ControllerHandlerType.PrizeStand) + { + PrizeStandController controller = new PrizeStandController( + deviceId, + (id, message) => _manager != null && _manager.SendToDevice(id, message), + message => GameManager.Logger?.Log("PrizeStandController", $"[{deviceId}] {message}")); + + controller.SetBoxesCount(binding.prizeStandBoxesCount); + return controller; + } + + return null; + } + + private RoleHandlerBinding FindRoleBinding(string role) + { + for (int i = 0; i < roleHandlerBindings.Length; i++) + { + RoleHandlerBinding binding = roleHandlerBindings[i]; + if (binding == null || string.IsNullOrWhiteSpace(binding.role)) + { + continue; + } + + if (string.Equals(binding.role.Trim(), role.Trim(), StringComparison.OrdinalIgnoreCase)) + { + return binding; + } + } + + return null; + } + + public void Blink() => _light.Blink(); + + public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat); + + private void Update() + { + if (_manager == null) return; + + int coinsProcessed = 0; + while (_manager.TryDequeueCoin(out _)) + { + AddCredit(); + coinsProcessed++; + } + + while (_manager.TryDequeueInput(out DeviceInputMessage msg)) + { + OnDeviceInputReceived?.Invoke(msg.DeviceId, msg.InputBits); + + if (msg.DeviceId == _manager.DefaultDeviceId) + { + OnInputReceived?.Invoke(msg.InputBits); + } + } + + while (_manager.TryDequeueAuthorized(out string deviceId)) + { + OnDeviceAuthorized?.Invoke(deviceId); + + if (!_initializeInvoked) + { + _initializeInvoked = true; + OnInitialize?.Invoke(); + } + + if (deviceId == _manager.DefaultDeviceId) + { + _light.Blink(); + } + } + + if (coinsProcessed > 0) + { + GameManager.Logger?.Log("ComSystemManager", $"Обработано монет: {coinsProcessed}"); + } + } + + private void OnDisable() + { + _manager?.ShutdownAll(false); + _manager = null; + _initializeInvoked = false; + } + + private void OnApplicationQuit() + { + _manager?.ShutdownAll(true); + _manager = null; + _initializeInvoked = false; + } } } + diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/ComWrapper.cs b/Assets/Scripts/Controls/Inputs/ComSystem/ComWrapper.cs index 725a0d7..d08d861 100644 --- a/Assets/Scripts/Controls/Inputs/ComSystem/ComWrapper.cs +++ b/Assets/Scripts/Controls/Inputs/ComSystem/ComWrapper.cs @@ -1,231 +1,14 @@ using System; -using RR.Game2D.Core.Managers; using UnityEngine; -using UnityEngine.Events; namespace RR.Common.Controls { /// - /// Совместимый фасад над мульти-устройственной COM-системой. - /// Старый API сохранен для текущего проекта. + /// Устаревшее имя компонента COM-системы. + /// Используйте . /// - public class ComWrapper : MonoBehaviour + [Obsolete("Use ComSystemManager instead")] + public class ComWrapper : ComSystemManager { - [Header("Основные настройки")] - [SerializeField] private bool saveMoneyBetweenSessions; - [SerializeField] private bool enablePrizeChecking; - [SerializeField] private bool keepAlive = true; - - [Header("Авторизация")] - [SerializeField] private bool enableEncryptedHandshake = true; - [SerializeField] private int handshakeTimeoutMs = 5000; - - [Header("Роль контроллера (после handshake)")] - [SerializeField] private string roleRequestCommand = "97"; - [SerializeField] private string roleResponsePrefix = "ROLE"; - [SerializeField] private int roleRequestTimeoutMs = 1500; - [SerializeField] private string mainControllerRole = "main_controller"; - - [Header("Мульти-устройства")] - [SerializeField] private bool useAllDetectedPorts = true; - [SerializeField] private string[] explicitPorts = Array.Empty(); - [SerializeField] private string defaultDevicePort = ""; - - public static UnityEvent OnCreditAdded; - public static UnityEvent OnCreditRemoved; - public static UnityEvent OnInputReceived; - public static Action OnInitialize; - - public static UnityEvent OnDeviceInputReceived; - public static UnityEvent OnDeviceAuthorized; - - private static ComWrapper _instance; - private static int _creditsCount; - - private ComMultiManager _manager; - private LightController _light; - private bool _initializeInvoked; - - public LightController Light => _light; - - public void Initialize() - { - _instance = this; - - OnCreditAdded = new UnityEvent(); - OnCreditRemoved = new UnityEvent(); - OnInputReceived = new UnityEvent(); - OnDeviceInputReceived = new UnityEvent(); - OnDeviceAuthorized = new UnityEvent(); - - if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0); - _creditsCount = PlayerPrefs.GetInt("CoinsCount", 0); - - _light = GetComponent() ?? gameObject.AddComponent(); - - _manager = new ComMultiManager( - useAllDetectedPorts, - explicitPorts, - defaultDevicePort, - enableEncryptedHandshake, - handshakeTimeoutMs, - keepAlive || enablePrizeChecking, - roleRequestCommand, - roleResponsePrefix, - roleRequestTimeoutMs, - mainControllerRole); - - _manager.InitializeDevices(); - } - - public static void AddCredit() - { - _creditsCount++; - PlayerPrefs.SetInt("CoinsCount", _creditsCount); - OnCreditAdded?.Invoke(_creditsCount); - } - - public static bool Pay(int amount = 1) - { - if (_creditsCount < amount) - { - return false; - } - - _creditsCount -= amount; - PlayerPrefs.SetInt("CoinsCount", _creditsCount); - - PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1); - PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1); - - OnCreditRemoved?.Invoke(_creditsCount); - return true; - } - - public static int GetCredits() => _creditsCount; - - public static void SetCredits(int amount) - { - int oldCredits = _creditsCount; - _creditsCount = Mathf.Max(0, amount); - PlayerPrefs.SetInt("CoinsCount", _creditsCount); - - if (_creditsCount > oldCredits) - { - OnCreditAdded?.Invoke(_creditsCount); - } - else if (_creditsCount < oldCredits) - { - OnCreditRemoved?.Invoke(_creditsCount); - } - } - - public static void Send(string message) - { - if (_instance == null || string.IsNullOrWhiteSpace(message)) return; - _instance._manager?.SendToDefault(message); - } - - public static void SendToDevice(string deviceId, string message) - { - if (_instance == null || string.IsNullOrWhiteSpace(message)) return; - _instance._manager?.SendToDevice(deviceId, message); - } - - public static void SendToAll(string message) - { - if (_instance == null || string.IsNullOrWhiteSpace(message)) return; - _instance._manager?.SendToAll(message); - } - - public static string GetLastInputState() - { - if (_instance?._manager == null) return "11111111111111111111111111"; - string defaultId = _instance._manager.DefaultDeviceId; - return string.IsNullOrWhiteSpace(defaultId) - ? "11111111111111111111111111" - : _instance._manager.GetLastInputState(defaultId); - } - - public static string GetLastInputState(string deviceId) - { - return _instance?._manager?.GetLastInputState(deviceId) ?? "11111111111111111111111111"; - } - - public static string[] GetConnectedDeviceIds() - { - return _instance?._manager?.ConnectedDeviceIds ?? Array.Empty(); - } - - public static string GetDefaultDeviceId() - { - return _instance?._manager?.DefaultDeviceId ?? string.Empty; - } - - public static string GetDeviceRole(string deviceId) - { - return _instance?._manager?.GetDeviceRole(deviceId) ?? string.Empty; - } - - public void Blink() => _light.Blink(); - - public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat); - - private void Update() - { - if (_manager == null) return; - - int coinsProcessed = 0; - while (_manager.TryDequeueCoin(out _)) - { - AddCredit(); - coinsProcessed++; - } - - while (_manager.TryDequeueInput(out DeviceInputMessage msg)) - { - OnDeviceInputReceived?.Invoke(msg.DeviceId, msg.InputBits); - - if (msg.DeviceId == _manager.DefaultDeviceId) - { - OnInputReceived?.Invoke(msg.InputBits); - } - } - - while (_manager.TryDequeueAuthorized(out string deviceId)) - { - OnDeviceAuthorized?.Invoke(deviceId); - - if (!_initializeInvoked) - { - _initializeInvoked = true; - OnInitialize?.Invoke(); - } - - if (deviceId == _manager.DefaultDeviceId) - { - _light.Blink(); - } - } - - if (coinsProcessed > 0) - { - GameManager.Logger?.Log("ComWrapper", $"Обработано монет: {coinsProcessed}"); - } - } - - private void OnDisable() - { - _manager?.ShutdownAll(false); - _manager = null; - _initializeInvoked = false; - } - - private void OnApplicationQuit() - { - _manager?.ShutdownAll(true); - _manager = null; - _initializeInvoked = false; - } } } diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/DefaultComControllerHandler.cs b/Assets/Scripts/Controls/Inputs/ComSystem/DefaultComControllerHandler.cs new file mode 100644 index 0000000..daadf1c --- /dev/null +++ b/Assets/Scripts/Controls/Inputs/ComSystem/DefaultComControllerHandler.cs @@ -0,0 +1,40 @@ +using System.Collections.Concurrent; + +namespace RR.Common.Controls +{ + internal sealed class DefaultComControllerHandler : IComControllerHandler + { + private readonly string _deviceId; + private readonly ConcurrentQueue _coinQueue; + private readonly ConcurrentQueue _inputQueue; + + public DefaultComControllerHandler( + string deviceId, + ConcurrentQueue coinQueue, + ConcurrentQueue inputQueue) + { + _deviceId = deviceId; + _coinQueue = coinQueue; + _inputQueue = inputQueue; + } + + public void HandleCoinAccepted() + { + _coinQueue.Enqueue(_deviceId); + } + + public void HandleInputReceived(string inputBits) + { + _inputQueue.Enqueue(new DeviceInputMessage(_deviceId, inputBits)); + } + + public void HandleRawLine(string line) + { + // Для контроллера по умолчанию обработка сырых строк не требуется. + } + + public void Dispose() + { + } + } +} diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/DefaultComControllerHandler.cs.meta b/Assets/Scripts/Controls/Inputs/ComSystem/DefaultComControllerHandler.cs.meta new file mode 100644 index 0000000..f84d4dd --- /dev/null +++ b/Assets/Scripts/Controls/Inputs/ComSystem/DefaultComControllerHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3aed53dc839b7134c91148aca38412db \ No newline at end of file diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/IComControllerHandler.cs b/Assets/Scripts/Controls/Inputs/ComSystem/IComControllerHandler.cs new file mode 100644 index 0000000..e567de7 --- /dev/null +++ b/Assets/Scripts/Controls/Inputs/ComSystem/IComControllerHandler.cs @@ -0,0 +1,11 @@ +using System; + +namespace RR.Common.Controls +{ + internal interface IComControllerHandler : IDisposable + { + void HandleCoinAccepted(); + void HandleInputReceived(string inputBits); + void HandleRawLine(string line); + } +} diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/IComControllerHandler.cs.meta b/Assets/Scripts/Controls/Inputs/ComSystem/IComControllerHandler.cs.meta new file mode 100644 index 0000000..d9b4c39 --- /dev/null +++ b/Assets/Scripts/Controls/Inputs/ComSystem/IComControllerHandler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4ff383aa1ebc9874c9019bf48d7bd5b8 \ No newline at end of file diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/LightController.cs b/Assets/Scripts/Controls/Inputs/ComSystem/LightController.cs index a0961bf..4e279e0 100644 --- a/Assets/Scripts/Controls/Inputs/ComSystem/LightController.cs +++ b/Assets/Scripts/Controls/Inputs/ComSystem/LightController.cs @@ -160,7 +160,7 @@ namespace RR.Common.Controls ? Mathf.Max(0.05f, interval + Random.Range(-blinkIntervalVariance, blinkIntervalVariance)) : interval; - yield return new WaitForSeconds(wait); + yield return new WaitForSecondsRealtime(wait); } } @@ -179,3 +179,4 @@ namespace RR.Common.Controls } } + diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/LightSequence.cs b/Assets/Scripts/Controls/Inputs/ComSystem/LightSequence.cs index b027e3b..86a09bd 100644 --- a/Assets/Scripts/Controls/Inputs/ComSystem/LightSequence.cs +++ b/Assets/Scripts/Controls/Inputs/ComSystem/LightSequence.cs @@ -82,7 +82,7 @@ namespace RR.Common.Controls _controller.SendCommand(step.Mode, step.DurationSeconds); if (step.Mode != LightMode.Off && step.DurationSeconds > 0) - yield return new WaitForSeconds(step.DurationSeconds); + yield return new WaitForSecondsRealtime(step.DurationSeconds); } } while (_looping); @@ -91,3 +91,4 @@ namespace RR.Common.Controls } } } + diff --git a/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs b/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs index 407bbc5..2af7da7 100644 --- a/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs +++ b/Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs @@ -1,315 +1,294 @@ -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using RR.Common.Controls; -using UnityEngine; -//Зависит от ComNativeWrapper и одноименного плагина -//рекомендуется соединение с таймаутом не меньше 10мс. Иначе вероятно busy loop и много алокаций. -public static class PrizeStandController +namespace RR.Common.Controls { - /// - /// По завершении инициализации. - /// bool - успех/неуспех. - /// string - сообщение в случае неуспеха - /// - public static event Action InitializationComplete; - /// - /// По завершении операции открытия. - /// bool - успех/неуспех. - /// string - сообщение в случае неуспеха - /// - public static event Action OpeningFinished; - - private const string REQUEST_STAND_STATUS = "25"; - private const string OPEN_BOX_PREFIX = "2501"; - private const float STATUS_REQUEST_TIMEOUT = 3; - private const float WAIT_OPEN_STATUS_TIMEOUT = 5; - - private static readonly IReadOnlyList READY_TO_OPEN_STATUSES = new[] { BoxStatus.OK, BoxStatus.OKplusCard }; - - private static int handle = -1; - private static int boxesCount; - private static readonly SemaphoreSlim _lock = new(1, 1); - private static bool initialized = false; - - private static ILogger _logger; - - #region PUBLIC API - /// - /// Инициализировать модуль витрины. - /// - /// Id хэндла полученное после ComNativeWrapper.Serial_Open - /// Количество физических коробок витрины - /// (опционально) Логгер - public static void Initialize(int handleId, int countOfBoxes, ILogger logger) + // Зависит от ComHub и обрабатывает контроллеры витрины по ROLE. + public sealed class PrizeStandController : IComControllerHandler { - bool hasError = false; - StringBuilder sb = new StringBuilder(); + public static readonly string[] SupportedRoles = { "prize_stand", "prize_stand_controller" }; - //Проверка хэндла. h<=0 - ошибка открытия порта - if (handleId <= 0) + /// + /// По завершении операции открытия. + /// bool - успех/неуспех. + /// string - сообщение в случае неуспеха + /// + public event Action OpeningFinished; + + private const string RequestStandStatus = "25"; + private const string OpenBoxPrefix = "2501"; + private const float StatusRequestTimeout = 3f; + private const float WaitOpenStatusTimeout = 5f; + + private static readonly IReadOnlyList ReadyToOpenStatuses = + new[] { BoxStatus.OK, BoxStatus.OKplusCard }; + + private readonly string _deviceId; + private readonly Func _sendToDevice; + private readonly Action _log; + private readonly SemaphoreSlim _lock = new(1, 1); + private readonly ConcurrentQueue _statusQueue = new(); + + private int _boxesCount; + + public PrizeStandController(string deviceId, Func sendToDevice, Action log) { - hasError = true; - sb.AppendLine($"[Модуль витрины] Переданный handle={handleId}. Ожидалось значение>0."); - } - else handle = handleId; - - //Проверка количества коробок - if (countOfBoxes <= 0) - { - hasError = true; - sb.AppendLine($"[Модуль витрины] Попытка задать количество коробок={countOfBoxes}. Ожидалось значение>0."); - } - else boxesCount = countOfBoxes; - - //Установка логгера - _logger = logger; - if (_logger == null) - sb.AppendLine($"[Модуль витрины] Получен логгер NULL. Если нужно логгирование, передайте дествующий логгер."); - - //Завершение - if (hasError) - { - initialized = false; - handle = -1; - boxesCount = 0; - - InitializationComplete?.Invoke(false, sb.ToString()); - } - else - { - initialized = true; - InitializationComplete?.Invoke(true, sb.ToString()); - } - } - - - /// - /// Открыть коробку по номеру - /// - /// Номер коробки (считаем с 1) - public static async Task OpenBoxAsync(int boxNumber) - { - if(!await _lock.WaitAsync(0)) - { - OpeningFinished?.Invoke(false, "Выдача уже в процессе"); - return; + _deviceId = deviceId; + _sendToDevice = sendToDevice; + _log = log; } - try + public void SetBoxesCount(int boxesCount) { - //Валидация - var validationResult = await CanExecuteOpenAsync(boxNumber); - if (!validationResult.OK) + _boxesCount = Math.Max(0, boxesCount); + } + + public async Task OpenBoxAsync(int boxNumber) + { + if (!await _lock.WaitAsync(0)) { - OpeningFinished?.Invoke(false, validationResult.Message); + OpeningFinished?.Invoke(false, "Выдача уже в процессе"); return; } - SendOpenCommand(boxNumber); + try + { + var validationResult = await CanExecuteOpenAsync(boxNumber); + if (!validationResult.OK) + { + OpeningFinished?.Invoke(false, validationResult.Message); + return; + } - //Обработка результатов - BoxStatus openStatus = await ReadBoxStatus(boxNumber, 1, WAIT_OPEN_STATUS_TIMEOUT); - if (openStatus == BoxStatus.OPENED) - { - OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта"); + ClearStatusQueue(); + SendCommand($"{OpenBoxPrefix}{boxNumber:D2}"); + + BoxStatus openStatus = await ReadBoxStatusAsync(boxNumber, 1, WaitOpenStatusTimeout); + if (openStatus == BoxStatus.OPENED) + { + OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта"); + } + else + { + _log?.Invoke($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}"); + OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}"); + } } - else + catch (Exception ex) { - _logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}"); - OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}"); + _log?.Invoke($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}"); + OpeningFinished?.Invoke(false, ex.Message); + } + finally + { + _lock.Release(); } } - catch (Exception ex) - { - _logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}"); - OpeningFinished?.Invoke(false, ex.Message); - } - finally - { - _lock.Release(); - } - } - #endregion - #region HELPERS - private static void SendOpenCommand(int boxNumber) - => ComNativeWrapper.Serial_WriteStringUTF8(handleId: handle, - utf8: $"{OPEN_BOX_PREFIX}{boxNumber:D2}", //D2 приводит в формат XX (2 знака, ноль впереди) - appendNewline: 1); - private static void SendRequestStatusCommand() - => ComNativeWrapper.Serial_WriteStringUTF8(handle, REQUEST_STAND_STATUS, 1); - - /// - /// Проверка может ли модуль произвести выдачу - /// - private static async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber) - { - bool isOk = true; - StringBuilder sb = new StringBuilder(); - sb.AppendLine($"[Модуль витрины. Проверка перед открытием]"); - - //Проверка инициализации - if (!initialized) + public void HandleCoinAccepted() { - isOk = false; - sb.AppendLine("- Модуль не инициализирован."); + // Для витрины монеты не обрабатываются. } - bool boxNumberIsValid = boxNumber > 0 && boxNumber <= boxesCount; - if (!boxNumberIsValid) + public void HandleInputReceived(string inputBits) { - isOk = false; - sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {boxesCount}."); + // Для витрины INPUT-пакеты не используются. } - //Не лезем в порт с запросом статуса коробки, если то, что выше дало НеОК - if (isOk) + public void HandleRawLine(string line) { - SendRequestStatusCommand(); - BoxStatus boxStatus = await ReadBoxStatus(boxNumber, boxesCount, STATUS_REQUEST_TIMEOUT); + var parseResult = ParseBoxStatus(line); + if (parseResult.BoxNumber < 0) + { + return; + } - if (!READY_TO_OPEN_STATUSES.Contains(boxStatus)) + _statusQueue.Enqueue(new BoxStatusEntry(parseResult.BoxNumber, parseResult.Status)); + } + + public void Dispose() + { + _lock.Dispose(); + ClearStatusQueue(); + } + + public static bool IsSupportedRole(string role) + { + if (string.IsNullOrWhiteSpace(role)) + { + return false; + } + + return SupportedRoles.Any(r => string.Equals(r, role, StringComparison.OrdinalIgnoreCase)); + } + + private async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber) + { + bool isOk = true; + StringBuilder sb = new(); + sb.AppendLine("[Модуль витрины. Проверка перед открытием]"); + + if (boxNumber <= 0) { isOk = false; - sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие."); - } - } - else - { - sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше."); - } - - return (isOk, sb.ToString()); - } - - - /// - /// Читать статус заданной ячейки витрины. Выход когда прочитали заданное количество вхождений, или по таймауту. - /// - /// Номер ячейки которую мы ждем - /// Сколько ячеек мы ждем - /// Таймаут операции(сек.) - /// - private static async Task ReadBoxStatus(int expectedBoxNumber, int expectedEntriesCount, float timeout) - { - byte[] buffer = new byte[1024]; - - //Изначально предполагаем что статус нужной коробки не пришел - BoxStatus status = BoxStatus.NotFound; - - int boxesRecieved = 0; - var sw = Stopwatch.StartNew(); - - while (sw.Elapsed.TotalSeconds < timeout && boxesRecieved < expectedEntriesCount) - { - //ждать пока всё не считаем - int entryLength = await Task.Run(() => ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buffer, buffer.Length)); - - if (entryLength == 0) //ничего не пришло - { - continue; - } - else if (entryLength < 0) //ошибка - { - _logger?.Log($"Ошибка при считывании статуса ячеек={entryLength} :: {ComNativeWrapper.GetLastError()}"); - continue; + sb.AppendLine("- Номер коробки должен быть больше 0."); } - - string entry = Encoding.UTF8.GetString(buffer, 0, entryLength).Trim(); - var ParseResult = ParseBoxStatus(entry); - - if(ParseResult.BoxNumber == -1) + if (_boxesCount > 0 && boxNumber > _boxesCount) { - _logger?.Log($"[Модуль витрины. Ошибка получения статуса ячейки]\n{ParseResult.Message}"); + isOk = false; + sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {_boxesCount}."); } - else + + if (isOk) { - boxesRecieved++; - if (ParseResult.BoxNumber == expectedBoxNumber) - status = ParseResult.Status; - } - } + ClearStatusQueue(); + SendCommand(RequestStandStatus); - if (status == BoxStatus.NotFound) - _logger?.Log($"[Модуль витрины. Не дождался ответа по статусу ячейки]\n" + - $"Ожидаемый номер: {expectedBoxNumber}\n" + - $"Получено статусов ячеек: {boxesRecieved}/{expectedEntriesCount}"); - return status; - } + int expectedEntriesCount = _boxesCount > 0 ? _boxesCount : 1; + BoxStatus boxStatus = await ReadBoxStatusAsync(boxNumber, expectedEntriesCount, StatusRequestTimeout); - private static (int BoxNumber, BoxStatus Status, string Message) ParseBoxStatus(string entryString) - { - int number = -1; - BoxStatus status = BoxStatus.ParsingError; - string message = ""; - - - - if (entryString.StartsWith("BOX")) - { - var noPrefixEntry = entryString.Substring(3); //Убираем "BOX" - string[] parts = noPrefixEntry.Split('_'); //ожидаем массив [<номер_коробки>, <статус_коробки>] - if (parts.Length == 2) - { - //Парсим номер коробки - bool boxNumberParsed = (int.TryParse(parts[0], out int parsedNumber)); - - if (boxNumberParsed) + if (!ReadyToOpenStatuses.Contains(boxStatus)) { - number = parsedNumber; - status = MapStatusFromString(parts[1]); + isOk = false; + sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие."); } - else message = $"Не удалось спарсить номер коробки: {entryString}"; } else { - message = $"Ошибка парсинга. Часть строки не пришла: {entryString}"; + sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше."); + } + + return (isOk, sb.ToString()); + } + + private async Task ReadBoxStatusAsync(int expectedBoxNumber, int expectedEntriesCount, float timeout) + { + BoxStatus status = BoxStatus.NotFound; + int boxesReceived = 0; + Stopwatch sw = Stopwatch.StartNew(); + + while (sw.Elapsed.TotalSeconds < timeout && boxesReceived < expectedEntriesCount) + { + while (_statusQueue.TryDequeue(out BoxStatusEntry entry)) + { + boxesReceived++; + if (entry.BoxNumber == expectedBoxNumber) + { + status = entry.Status; + } + + if (boxesReceived >= expectedEntriesCount) + { + break; + } + } + + if (boxesReceived >= expectedEntriesCount) + { + break; + } + + await Task.Delay(10); + } + + if (status == BoxStatus.NotFound) + { + _log?.Invoke("[Модуль витрины. Не дождался ответа по статусу ячейки]\n" + + $"Ожидаемый номер: {expectedBoxNumber}\n" + + $"Получено статусов ячеек: {boxesReceived}/{expectedEntriesCount}"); + } + + return status; + } + + private void SendCommand(string command) + { + if (!_sendToDevice(_deviceId, command)) + { + throw new InvalidOperationException($"Не удалось отправить команду '{command}' в устройство {_deviceId}"); } } - else message = $"Неожиданный префикс при парсинге ячеек: {entryString}"; + private void ClearStatusQueue() + { + while (_statusQueue.TryDequeue(out _)) + { + } + } - return (number, status, message); + private static (int BoxNumber, BoxStatus Status) ParseBoxStatus(string entryString) + { + if (string.IsNullOrWhiteSpace(entryString) || !entryString.StartsWith("BOX", StringComparison.OrdinalIgnoreCase)) + { + return (-1, BoxStatus.ParsingError); + } + string noPrefixEntry = entryString.Substring(3); + string[] parts = noPrefixEntry.Split('_'); + if (parts.Length != 2) + { + return (-1, BoxStatus.ParsingError); + } + + if (!int.TryParse(parts[0], out int parsedNumber)) + { + return (-1, BoxStatus.ParsingError); + } + + return (parsedNumber, MapStatusFromString(parts[1])); + } + + private static BoxStatus MapStatusFromString(string statusString) + { + return statusString switch + { + "OK" => BoxStatus.OK, + "OK+CARD" => BoxStatus.OKplusCard, + "EMPTY" => BoxStatus.Empty, + "OPEN" => BoxStatus.Open, + "OPENED" => BoxStatus.OPENED, + "OPEN_ERROR" => BoxStatus.OPEN_ERROR, + "ERROR" => BoxStatus.Error, + _ => BoxStatus.ParsingError + }; + } + + private readonly struct BoxStatusEntry + { + public readonly int BoxNumber; + public readonly BoxStatus Status; + + public BoxStatusEntry(int boxNumber, BoxStatus status) + { + BoxNumber = boxNumber; + Status = status; + } + } } - private static BoxStatus MapStatusFromString(string statusString) => statusString switch + + public enum BoxStatus { - "OK" => BoxStatus.OK, - "OK+CARD" => BoxStatus.OKplusCard, - "EMPTY" => BoxStatus.Empty, - "OPEN" => BoxStatus.Open, + // Статусы ДО открытия + OK, + OKplusCard, + Empty, + Open, - "OPENED" => BoxStatus.OPENED, - "OPEN_ERROR" => BoxStatus.OPEN_ERROR, + // Статусы ПОСЛЕ открытия + OPENED, + OPEN_ERROR, - "ERROR" => BoxStatus.Error, - - _ => BoxStatus.ParsingError - }; - #endregion + // Служебные статусы + ParsingError, + NotFound, + Error + } } -enum BoxStatus -{ - //Статусы ДО открытия - OK, //Готов к выдаче - OKplusCard, //Готов к выдаче + готов выдать по карте - Empty, //Пусто, уже выдали - Open, //Дверка УЖЕ открыта. Приз предположительно пустой - - //Статусы ПОСЛЕ открытия - OPENED, //Всё хорошо, открылся - OPEN_ERROR, //Ошибка открытия - - //Служебные статусы - ParsingError, //При парсинге что то пошло не так, или пришла непредусмотренная фигня - NotFound, //Коробка с требуемым номером не найдена - Error //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок) -} diff --git a/Assets/Scripts/Managers/GameManager.cs b/Assets/Scripts/Managers/GameManager.cs index 3d6c6d1..f1197ca 100644 --- a/Assets/Scripts/Managers/GameManager.cs +++ b/Assets/Scripts/Managers/GameManager.cs @@ -34,8 +34,8 @@ namespace RR.Game2D.Core.Managers // Plate control for game public ComSystemManager Plate { get; private set; } // Light control for game - public LightController Light => Plate != null ? Plate.Light : null; - + public LightController Light { get; private set; } + // Globals [Header("Globals")] [SerializeField] private string globalsPrefabResourcePath = "Prefabs/Global/Globals"; @@ -158,10 +158,16 @@ namespace RR.Game2D.Core.Managers if (Inactivity == null) Inactivity = gameObject.AddComponent(); if (Inactivity == null) Inactivity = InactivityManager.Instance; - // Apply loaded settings (new system applies automatically in Awake) - if (Plate == null) Plate = globals.GetComponent(); + if (Plate != null) Light = Plate.Light; + if (Light) Light.SetLight(LightMode.DiagonalWave) + .Then(LightMode.DiagonalWave2) + .Then(LightMode.Rainbow) + .Then(LightMode.FallingRows) + .Then(LightMode.SeaWave) + .Loop(); + Payment.OnCreditAdded.AddListener(OnCreditAdded); _gameStartTime = Time.unscaledTime; @@ -174,8 +180,11 @@ namespace RR.Game2D.Core.Managers private void OnDestroy() { + if (!Light) return; + Light.StopBlink(false); + Light.StopSequence(); + if (Instance == this) Instance = null; - Plate?.StopBlink(false); } private void OnApplicationQuit() @@ -331,8 +340,13 @@ public void TogglePause() => SetPause(!IsPaused); _gameStartTime = Time.time; Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}"); - if (Plate) Plate.StopBlink(); - Light?.SetLight(LightMode.Rainbow, 99); + // Активируем шапку и эффекты при начале игры + if (Light) + { + Light.StopBlink(); + if (Light.IsSequencePlaying) Light.StopSequence(); + Light.SetLight(LightMode.Rainbow).Loop(); + } // Включаем отслеживание бездействия if (Inactivity != null) Inactivity.EnableInactivityCheck(); diff --git a/Packages/manifest.json b/Packages/manifest.json index 907b99a..3efee6b 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,7 +1,6 @@ { "dependencies": { "com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity", - "com.quaza.unitymcp": "https://github.com/quazaai/UnityMCPIntegration.git", "com.unity.ai.inference": "2.5.0", "com.unity.cinemachine": "3.1.5", "com.unity.collab-proxy": "2.11.3", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index c683b6c..51ac49d 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -7,13 +7,6 @@ "dependencies": {}, "hash": "34432703a177c97631a4eb1d908f086037c67969" }, - "com.quaza.unitymcp": { - "version": "https://github.com/quazaai/UnityMCPIntegration.git", - "depth": 0, - "source": "git", - "dependencies": {}, - "hash": "d1515b326e2fc14b39de5a08534159ba29963c7d" - }, "com.unity.2d.animation": { "version": "13.0.4", "depth": 1,