ADD : 스트림덱 기능 추가 패치

This commit is contained in:
KINDNICK 2025-10-28 00:02:17 +09:00
parent 128e8771ed
commit d01b815746
50 changed files with 2145 additions and 87 deletions

BIN
.claude/settings.local.json (Stored with Git LFS)

Binary file not shown.

View File

@ -493,6 +493,35 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour
}
}
/// <summary>
/// 페이셜 모션 캡처 재접속
/// </summary>
public void Reconnect()
{
Debug.Log("[iFacialMocap] 재접속 시도 중...");
try
{
// 기존 연결 종료
StopUDP();
// 잠시 대기
Thread.Sleep(500);
// 플래그 리셋
StartFlag = true;
// 재시작
StartFunction();
Debug.Log("[iFacialMocap] 재접속 완료");
}
catch (Exception e)
{
Debug.LogError($"[iFacialMocap] 재접속 실패: {e.Message}");
}
}
private bool HasBlendShapes(SkinnedMeshRenderer skin)
{
if (!skin.sharedMesh)

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ae0a68acee725e141b02318f249f7990
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -10,6 +10,7 @@ namespace Streamingle.Editor
private bool createEventController = true;
private bool createStreamDeckManager = true;
private bool createAvatarOutfitController = true;
private bool createSystemController = true;
private string parentObjectName = "Streamingle 컨트롤러들";
private bool createParentObject = true;
@ -26,6 +27,7 @@ namespace Streamingle.Editor
private ItemController existingItemController;
private EventController existingEventController;
private AvatarOutfitController existingAvatarOutfitController;
private SystemController existingSystemController;
[MenuItem("Tools/Streamingle/고급 컨트롤러 설정 도구")]
public static void ShowWindow()
@ -65,6 +67,7 @@ namespace Streamingle.Editor
GUILayout.Label("생성할 컨트롤러들", EditorStyles.boldLabel);
createStreamDeckManager = EditorGUILayout.Toggle("StreamDeck 서버 매니저", createStreamDeckManager);
createSystemController = EditorGUILayout.Toggle("시스템 컨트롤러", createSystemController);
createCameraManager = EditorGUILayout.Toggle("카메라 매니저", createCameraManager);
createItemController = EditorGUILayout.Toggle("아이템 컨트롤러", createItemController);
createEventController = EditorGUILayout.Toggle("이벤트 컨트롤러", createEventController);
@ -199,6 +202,19 @@ namespace Streamingle.Editor
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
GUILayout.Label("시스템 컨트롤러:", GUILayout.Width(200));
if (existingSystemController != null)
{
string parentInfo = GetParentInfo(existingSystemController.transform);
EditorGUILayout.LabelField($"✓ 발견됨 {parentInfo}", EditorStyles.boldLabel);
}
else
{
EditorGUILayout.LabelField("✗ 발견되지 않음", EditorStyles.boldLabel);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
@ -221,6 +237,7 @@ namespace Streamingle.Editor
existingItemController = FindObjectOfType<ItemController>();
existingEventController = FindObjectOfType<EventController>();
existingAvatarOutfitController = FindObjectOfType<AvatarOutfitController>();
existingSystemController = FindObjectOfType<SystemController>();
}
private void CreateControllers()
@ -240,6 +257,12 @@ namespace Streamingle.Editor
CreateStreamDeckManager(parentObject);
}
// System Controller 생성
if (createSystemController && existingSystemController == null)
{
CreateSystemController(parentObject);
}
// Camera Manager 생성
if (createCameraManager && existingCameraManager == null)
{
@ -338,6 +361,14 @@ namespace Streamingle.Editor
UnityEngine.Debug.Log($"아바타 의상 컨트롤러를 {parent.name} 하위로 이동");
}
// System Controller 이동
if (existingSystemController != null && existingSystemController.transform.parent != parent.transform)
{
existingSystemController.transform.SetParent(parent.transform);
movedCount++;
UnityEngine.Debug.Log($"시스템 컨트롤러를 {parent.name} 하위로 이동");
}
if (movedCount > 0)
{
UnityEngine.Debug.Log($"{movedCount}개의 컨트롤러를 {parent.name} 하위로 이동했습니다.");
@ -404,6 +435,16 @@ namespace Streamingle.Editor
}
}
// System Controller 연결
if (existingSystemController != null)
{
var systemControllerProperty = serializedObject.FindProperty("systemController");
if (systemControllerProperty != null)
{
systemControllerProperty.objectReferenceValue = existingSystemController;
}
}
serializedObject.ApplyModifiedProperties();
UnityEngine.Debug.Log("기존 컨트롤러들을 StreamDeck 서버 매니저에 연결했습니다!");
@ -566,5 +607,48 @@ namespace Streamingle.Editor
UnityEngine.Debug.Log("아바타 의상 컨트롤러 생성됨");
}
private void CreateSystemController(GameObject parent)
{
GameObject systemControllerObject = new GameObject("시스템 컨트롤러");
if (parent != null)
{
systemControllerObject.transform.SetParent(parent.transform);
}
// SystemController 스크립트 추가
var systemController = systemControllerObject.AddComponent<SystemController>();
// 기본 설정
SerializedObject serializedObject = new SerializedObject(systemController);
serializedObject.Update();
// OptiTrack 클라이언트 자동 찾기는 Start()에서 수행됨
// Motion Recorder 자동 찾기 활성화
var autoFindRecordersProperty = serializedObject.FindProperty("autoFindRecorders");
if (autoFindRecordersProperty != null)
{
autoFindRecordersProperty.boolValue = true;
}
// Facial Motion 클라이언트 자동 찾기 활성화
var autoFindFacialMotionClientsProperty = serializedObject.FindProperty("autoFindFacialMotionClients");
if (autoFindFacialMotionClientsProperty != null)
{
autoFindFacialMotionClientsProperty.boolValue = true;
}
// 디버그 로그 활성화
var enableDebugLogProperty = serializedObject.FindProperty("enableDebugLog");
if (enableDebugLogProperty != null)
{
enableDebugLogProperty.boolValue = true;
}
serializedObject.ApplyModifiedProperties();
UnityEngine.Debug.Log("시스템 컨트롤러 생성됨");
}
}
}

View File

@ -147,7 +147,7 @@ Material:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _Resolution: {r: 1101, g: 514, b: 0, a: 0}
- _Resolution: {r: 1920, g: 1080, b: 0, a: 0}
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -157,6 +157,20 @@
"m_SlotId": 1
}
},
{
"m_OutputSlot": {
"m_Node": {
"m_Id": "a545499797a14671a5d598434d7c85bc"
},
"m_SlotId": 4
},
"m_InputSlot": {
"m_Node": {
"m_Id": "ad863a97e6f14704b6137da83d1dba35"
},
"m_SlotId": 0
}
},
{
"m_OutputSlot": {
"m_Node": {
@ -346,6 +360,8 @@
"overrideHLSLDeclaration": false,
"hlslDeclarationOverride": 0,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": {
"x": 1920.0,
"y": 1080.0,
@ -465,12 +481,15 @@
"overrideHLSLDeclaration": true,
"hlslDeclarationOverride": 2,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": {
"m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}",
"m_SerializedTexture": "",
"m_Guid": ""
},
"isMainTexture": false,
"useTilingAndOffset": false,
"useTexelSize": true,
"m_Modifiable": true,
"m_DefaultType": 0
}
@ -759,9 +778,9 @@
"m_Position": {
"serializedVersion": "2",
"x": -1113.0,
"y": 287.0,
"y": 301.0,
"width": 145.0,
"height": 129.00003051757813
"height": 128.0
}
},
"m_Slots": [
@ -845,7 +864,7 @@
"m_StageCapability": 3,
"m_BareResource": false,
"m_Texture": {
"m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}",
"m_SerializedTexture": "",
"m_Guid": ""
},
"m_DefaultType": 0
@ -1256,7 +1275,7 @@
"m_StageCapability": 3,
"m_BareResource": false,
"m_Texture": {
"m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}",
"m_SerializedTexture": "",
"m_Guid": ""
},
"m_DefaultType": 0
@ -1321,10 +1340,10 @@
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -848.0000610351563,
"y": 365.0000305175781,
"x": -851.0,
"y": 364.9999694824219,
"width": 281.0,
"height": 190.00003051757813
"height": 190.00009155273438
}
},
"m_Slots": [
@ -1360,7 +1379,8 @@
},
"m_SourceType": 0,
"m_FunctionName": "AAFromGreenChannel",
"m_FunctionSource": "f61580200f158d84880df345e2130e9b",
"m_FunctionSource": "7f3a4e8b9c2d1a5e6f8b9c2d1a5e6f8b",
"m_FunctionSourceUsePragmas": true,
"m_FunctionBody": "Enter function body here..."
}
@ -1609,10 +1629,10 @@
"m_Expanded": true,
"m_Position": {
"serializedVersion": "2",
"x": -1099.0001220703125,
"y": 569.0000610351563,
"width": 131.00006103515626,
"height": 33.99993896484375
"x": -1099.0,
"y": 498.0,
"width": 131.0,
"height": 34.0
}
},
"m_Slots": [
@ -1646,8 +1666,8 @@
"m_Position": {
"serializedVersion": "2",
"x": -1145.0,
"y": 444.0000305175781,
"width": 176.99993896484376,
"y": 429.0,
"width": 177.0,
"height": 34.0
}
},
@ -1719,12 +1739,15 @@
"overrideHLSLDeclaration": true,
"hlslDeclarationOverride": 1,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": {
"m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}",
"m_SerializedTexture": "",
"m_Guid": ""
},
"isMainTexture": false,
"useTilingAndOffset": false,
"useTexelSize": true,
"m_Modifiable": true,
"m_DefaultType": 1
}
@ -1763,8 +1786,8 @@
"m_Position": {
"serializedVersion": "2",
"x": -1089.0,
"y": 521.0000610351563,
"width": 120.99993896484375,
"y": 464.0,
"width": 121.0,
"height": 34.0
}
},
@ -1873,12 +1896,24 @@
"overrideHLSLDeclaration": false,
"hlslDeclarationOverride": 0,
"m_Hidden": false,
"m_PerRendererData": false,
"m_customAttributes": [],
"m_Value": 0.0,
"m_FloatType": 0,
"m_RangeValues": {
"x": 0.0,
"y": 1.0
}
},
"m_SliderType": 0,
"m_SliderPower": 3.0,
"m_EnumType": 0,
"m_CSharpEnumString": "",
"m_EnumNames": [
"Default"
],
"m_EnumValues": [
0
]
}
{

View File

@ -0,0 +1,115 @@
#ifndef AA_FROM_GREEN_CHANNEL_INCLUDED
#define AA_FROM_GREEN_CHANNEL_INCLUDED
// Shader Graph Custom Function
// UnityTexture2D와 UnitySamplerState를 받아서 G 채널에 안티에일리어싱 적용
void AAFromGreenChannel_float(
float2 UV,
UnityTexture2D Tex,
UnitySamplerState Samp,
float Strength,
float2 Resolution,
out float New)
{
// Texel Size 계산
float2 texelSize = 1.0 / Resolution;
// 중앙값
float center = SAMPLE_TEXTURE2D(Tex, Samp, UV).g;
if (Strength > 0.0)
{
// 주변 8방향 샘플링
float left = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, 0)).g;
float right = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, 0)).g;
float top = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, texelSize.y)).g;
float bottom = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, -texelSize.y)).g;
float topLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, texelSize.y)).g;
float topRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, texelSize.y)).g;
float bottomLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, -texelSize.y)).g;
float bottomRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, -texelSize.y)).g;
// 9탭 박스 필터 평균
float average = (center + left + right + top + bottom +
topLeft + topRight + bottomLeft + bottomRight) / 9.0;
// Strength에 따라 블렌딩
New = lerp(center, average, saturate(Strength));
}
else
{
New = center;
}
}
// Simple 버전 - 5탭 크로스 필터
void AAFromGreenChannelSimple_float(
float2 UV,
UnityTexture2D Tex,
UnitySamplerState Samp,
float Strength,
float2 Resolution,
out float New)
{
float2 texelSize = 1.0 / Resolution;
float center = SAMPLE_TEXTURE2D(Tex, Samp, UV).g;
if (Strength > 0.0)
{
float left = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, 0)).g;
float right = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, 0)).g;
float top = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, texelSize.y)).g;
float bottom = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, -texelSize.y)).g;
float average = (center + left + right + top + bottom) / 5.0;
New = lerp(center, average, saturate(Strength));
}
else
{
New = center;
}
}
// Weighted 버전 - 가우시안 가중치
void AAFromGreenChannelWeighted_float(
float2 UV,
UnityTexture2D Tex,
UnitySamplerState Samp,
float Strength,
float2 Resolution,
out float New)
{
float2 texelSize = 1.0 / Resolution;
float center = SAMPLE_TEXTURE2D(Tex, Samp, UV).g;
if (Strength > 0.0)
{
float left = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, 0)).g;
float right = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, 0)).g;
float top = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, texelSize.y)).g;
float bottom = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, -texelSize.y)).g;
float topLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, texelSize.y)).g;
float topRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, texelSize.y)).g;
float bottomLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, -texelSize.y)).g;
float bottomRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, -texelSize.y)).g;
// 가우시안 가중치
float weighted = center * 0.25;
weighted += (left + right + top + bottom) * 0.125;
weighted += (topLeft + topRight + bottomLeft + bottomRight) * 0.0625;
New = lerp(center, weighted, saturate(Strength));
}
else
{
New = center;
}
}
#endif // AA_FROM_GREEN_CHANNEL_INCLUDED

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7f3a4e8b9c2d1a5e6f8b9c2d1a5e6f8b
ShaderIncludeImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,93 @@
Shader "Hidden/AlphaFromNiloToon"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" {}
_AlphaTex ("Alpha Texture (NiloToon Prepass)", 2D) = "white" {}
_BlurRadius ("Blur Radius", Float) = 1.0
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
LOD 100
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
sampler2D _AlphaTex;
float4 _MainTex_ST;
float4 _AlphaTex_TexelSize;
float _BlurRadius;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
// 3x3 가우시안 블러 커널 (매우 가벼운 블러)
fixed GaussianBlurAlpha(float2 uv)
{
float2 texelSize = _AlphaTex_TexelSize.xy * _BlurRadius;
// 가우시안 가중치 (3x3 커널)
fixed alpha = 0.0;
// 중앙 픽셀 (가장 높은 가중치)
alpha += tex2D(_AlphaTex, uv).g * 0.25;
// 상하좌우 (중간 가중치)
alpha += tex2D(_AlphaTex, uv + float2(0, texelSize.y)).g * 0.125;
alpha += tex2D(_AlphaTex, uv + float2(0, -texelSize.y)).g * 0.125;
alpha += tex2D(_AlphaTex, uv + float2(texelSize.x, 0)).g * 0.125;
alpha += tex2D(_AlphaTex, uv + float2(-texelSize.x, 0)).g * 0.125;
// 대각선 (낮은 가중치)
alpha += tex2D(_AlphaTex, uv + float2(texelSize.x, texelSize.y)).g * 0.0625;
alpha += tex2D(_AlphaTex, uv + float2(-texelSize.x, texelSize.y)).g * 0.0625;
alpha += tex2D(_AlphaTex, uv + float2(texelSize.x, -texelSize.y)).g * 0.0625;
alpha += tex2D(_AlphaTex, uv + float2(-texelSize.x, -texelSize.y)).g * 0.0625;
return alpha;
}
fixed4 frag (v2f i) : SV_Target
{
// 메인 텍스처에서 RGB 가져오기
fixed4 col = tex2D(_MainTex, i.uv);
// NiloToon Prepass 버퍼의 G 채널에서 알파 가져오기 (가우시안 블러 적용)
fixed alpha = GaussianBlurAlpha(i.uv);
// RGB는 그대로, 알파는 블러 처리된 값 사용
col.a = alpha;
return col;
}
ENDCG
}
}
}

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: bac6acd56892cc94ba00d208d4d82712
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@ -16,6 +16,7 @@ public class StreamDeckServerManager : MonoBehaviour
public ItemController itemController { get; private set; }
public EventController eventController { get; private set; }
public AvatarOutfitController avatarOutfitController { get; private set; }
public SystemController systemController { get; private set; }
// 싱글톤 패턴으로 StreamDeckService에서 접근 가능하도록
public static StreamDeckServerManager Instance { get; private set; }
@ -60,6 +61,13 @@ public class StreamDeckServerManager : MonoBehaviour
Debug.LogWarning("[StreamDeckServerManager] AvatarOutfitController를 찾을 수 없습니다. 아바타 의상 컨트롤 기능이 비활성화됩니다.");
}
// SystemController 찾기
systemController = FindObjectOfType<SystemController>();
if (systemController == null)
{
Debug.LogWarning("[StreamDeckServerManager] SystemController를 찾을 수 없습니다. 시스템 컨트롤 기능이 비활성화됩니다.");
}
StartServer();
}
@ -335,6 +343,23 @@ public class StreamDeckServerManager : MonoBehaviour
HandleGetAvatarOutfitList(service);
break;
// SystemController 명령어들
case "toggle_optitrack_markers":
case "show_optitrack_markers":
case "hide_optitrack_markers":
case "reconnect_optitrack":
case "reconnect_facial_motion":
case "refresh_facial_motion_clients":
case "start_motion_recording":
case "stop_motion_recording":
case "toggle_motion_recording":
case "refresh_motion_recorders":
case "capture_screenshot":
case "capture_alpha_screenshot":
case "open_screenshot_folder":
HandleSystemCommand(message);
break;
case "test":
// 테스트 메시지 에코 응답
var response = new
@ -961,6 +986,43 @@ public class StreamDeckServerManager : MonoBehaviour
}
}
private void HandleSystemCommand(Dictionary<string, object> message)
{
string messageType = message.ContainsKey("type") ? message["type"].ToString() : null;
Debug.Log($"[StreamDeckServerManager] 시스템 명령어 실행: {messageType}");
if (systemController == null)
{
Debug.LogError("[StreamDeckServerManager] SystemController가 null입니다!");
return;
}
try
{
// 파라미터 추출 (있을 경우)
Dictionary<string, object> parameters = new Dictionary<string, object>();
if (message.ContainsKey("data"))
{
var dataObject = message["data"];
if (dataObject is Newtonsoft.Json.Linq.JObject jObject)
{
foreach (var prop in jObject.Properties())
{
parameters[prop.Name] = prop.Value.ToString();
}
}
}
// SystemController의 ExecuteCommand 호출
systemController.ExecuteCommand(messageType, parameters);
}
catch (Exception ex)
{
Debug.LogError($"[StreamDeckServerManager] 시스템 명령어 실행 실패: {ex.Message}");
}
}
private void HandleGetAvatarOutfitList(StreamDeckService service)
{
@ -1009,7 +1071,9 @@ public class StreamDeckService : WebSocketBehavior
protected override void OnMessage(WebSocketSharp.MessageEventArgs e)
{
Debug.Log($"[StreamDeckService] 원본 메시지 수신: {e.Data}");
string timestamp = System.DateTime.Now.ToString("HH:mm:ss.fff");
Debug.Log($"[{timestamp}] [StreamDeckService] 원본 메시지 수신: {e.Data}");
Debug.Log($"[{timestamp}] [StreamDeckService] ⚠️ 지금 바로 버튼을 클릭했나요? 클릭했다면 어떤 버튼인지 확인하세요!");
// 메인 스레드에서 처리하도록 매니저에게 전달
if (serverManager != null)

View File

@ -0,0 +1,773 @@
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System;
using Entum;
/// <summary>
/// StreamDeck 단일 기능 버튼들을 통합 관리하는 시스템 컨트롤러
/// 각 기능은 고유 ID로 식별되며, 확장이 용이한 구조
/// </summary>
public class SystemController : MonoBehaviour
{
[Header("OptiTrack 참조")]
public OptitrackStreamingClient optitrackClient;
[Header("모션 녹화 설정")]
[Tooltip("모션 녹화 시 OptiTrack Motive도 함께 녹화할지 여부")]
public bool recordOptiTrackWithMotion = true;
[Header("EasyMotion Recorder")]
[Tooltip("true면 씬의 모든 MotionDataRecorder를 자동으로 찾습니다")]
public bool autoFindRecorders = true;
[Tooltip("수동으로 지정할 레코더 목록 (autoFindRecorders가 false일 때 사용)")]
public List<MotionDataRecorder> motionRecorders = new List<MotionDataRecorder>();
[Header("Facial Motion Capture")]
[Tooltip("true면 씬의 모든 페이셜 모션 클라이언트를 자동으로 찾습니다")]
public bool autoFindFacialMotionClients = true;
[Tooltip("수동으로 지정할 페이셜 모션 클라이언트 목록 (autoFindFacialMotionClients가 false일 때 사용)")]
public List<UnityRecieve_FACEMOTION3D_and_iFacialMocap> facialMotionClients = new List<UnityRecieve_FACEMOTION3D_and_iFacialMocap>();
[Header("Screenshot Settings")]
[Tooltip("스크린샷 해상도 (기본: 4K)")]
public int screenshotWidth = 3840;
[Tooltip("스크린샷 해상도 (기본: 4K)")]
public int screenshotHeight = 2160;
[Tooltip("스크린샷 저장 경로 (비어있으면 바탕화면)")]
public string screenshotSavePath = "";
[Tooltip("파일명 앞에 붙을 접두사")]
public string screenshotFilePrefix = "Screenshot";
[Tooltip("알파 채널 추출용 셰이더")]
public Shader alphaShader;
[Tooltip("NiloToon Prepass 버퍼 텍스처 이름")]
public string niloToonPrepassBufferName = "_NiloToonPrepassBufferTex";
[Tooltip("촬영할 카메라 (비어있으면 메인 카메라 사용)")]
public Camera screenshotCamera;
[Tooltip("알파 채널 블러 반경 (0 = 블러 없음, 1.0 = 약한 블러)")]
[Range(0f, 3f)]
public float alphaBlurRadius = 1.0f;
[Header("디버그")]
public bool enableDebugLog = true;
private bool isRecording = false;
private Material alphaMaterial;
// 싱글톤 패턴
public static SystemController Instance { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
private void Start()
{
// OptiTrack 클라이언트 자동 찾기
if (optitrackClient == null)
{
optitrackClient = FindObjectOfType<OptitrackStreamingClient>();
}
// Motion Recorder 자동 찾기
if (autoFindRecorders)
{
RefreshMotionRecorders();
}
// Facial Motion 클라이언트 자동 찾기
if (autoFindFacialMotionClients)
{
RefreshFacialMotionClients();
}
// Screenshot 설정 초기화
if (screenshotCamera == null)
{
screenshotCamera = Camera.main;
}
if (alphaShader == null)
{
alphaShader = Shader.Find("Hidden/AlphaFromNiloToon");
if (alphaShader == null)
{
LogError("알파 셰이더를 찾을 수 없습니다: Hidden/AlphaFromNiloToon");
}
}
if (string.IsNullOrEmpty(screenshotSavePath))
{
screenshotSavePath = Path.Combine(Application.dataPath, "..", "Screenshots");
}
// Screenshots 폴더가 없으면 생성
if (!Directory.Exists(screenshotSavePath))
{
Directory.CreateDirectory(screenshotSavePath);
Log($"Screenshots 폴더 생성됨: {screenshotSavePath}");
}
Log("SystemController 초기화 완료");
Log($"Motion Recorder 개수: {motionRecorders.Count}");
Log($"Facial Motion 클라이언트 개수: {facialMotionClients.Count}");
Log($"Screenshot 카메라: {(screenshotCamera != null ? "" : "")}");
}
/// <summary>
/// 씬에서 모든 MotionDataRecorder를 다시 찾습니다
/// </summary>
public void RefreshMotionRecorders()
{
var allRecorders = FindObjectsOfType<MotionDataRecorder>();
motionRecorders = allRecorders.ToList();
Log($"Motion Recorder {motionRecorders.Count}개 발견");
}
#region OptiTrack
/// <summary>
/// OptiTrack 마커 표시 토글 (켜기/끄기)
/// </summary>
public void ToggleOptitrackMarkers()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
optitrackClient.ToggleDrawMarkers();
Log($"OptiTrack 마커 표시: {optitrackClient.DrawMarkers}");
}
/// <summary>
/// OptiTrack 마커 표시 켜기
/// </summary>
public void ShowOptitrackMarkers()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
if (!optitrackClient.DrawMarkers)
{
optitrackClient.ToggleDrawMarkers();
}
Log("OptiTrack 마커 표시 켜짐");
}
/// <summary>
/// OptiTrack 마커 표시 끄기
/// </summary>
public void HideOptitrackMarkers()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
if (optitrackClient.DrawMarkers)
{
optitrackClient.ToggleDrawMarkers();
}
Log("OptiTrack 마커 표시 꺼짐");
}
#endregion
#region OptiTrack
/// <summary>
/// OptiTrack 서버에 재접속 시도
/// </summary>
public void ReconnectOptitrack()
{
if (optitrackClient == null)
{
LogError("OptitrackStreamingClient를 찾을 수 없습니다!");
return;
}
Log("OptiTrack 재접속 시도...");
optitrackClient.Reconnect();
Log("OptiTrack 재접속 명령 전송 완료");
}
/// <summary>
/// OptiTrack 연결 상태 확인
/// </summary>
public bool IsOptitrackConnected()
{
if (optitrackClient == null)
{
return false;
}
return optitrackClient.IsConnected();
}
/// <summary>
/// OptiTrack 연결 상태를 문자열로 반환
/// </summary>
public string GetOptitrackConnectionStatus()
{
if (optitrackClient == null)
{
return "OptiTrack 클라이언트 없음";
}
return optitrackClient.GetConnectionStatus();
}
#endregion
#region Facial Motion Capture
/// <summary>
/// 씬에서 모든 Facial Motion 클라이언트를 다시 찾습니다
/// </summary>
public void RefreshFacialMotionClients()
{
var allClients = FindObjectsOfType<UnityRecieve_FACEMOTION3D_and_iFacialMocap>();
facialMotionClients = allClients.ToList();
Log($"Facial Motion 클라이언트 {facialMotionClients.Count}개 발견");
}
/// <summary>
/// 모든 Facial Motion 클라이언트 재접속
/// </summary>
public void ReconnectFacialMotion()
{
if (autoFindFacialMotionClients)
{
RefreshFacialMotionClients();
}
if (facialMotionClients == null || facialMotionClients.Count == 0)
{
LogError("Facial Motion 클라이언트가 없습니다!");
return;
}
Log($"Facial Motion 클라이언트 재접속 시도... ({facialMotionClients.Count}개)");
int reconnectedCount = 0;
foreach (var client in facialMotionClients)
{
if (client != null)
{
try
{
client.Reconnect();
reconnectedCount++;
Log($"클라이언트 재접속 성공: {client.gameObject.name}");
}
catch (System.Exception e)
{
LogError($"클라이언트 재접속 실패 ({client.gameObject.name}): {e.Message}");
}
}
}
if (reconnectedCount > 0)
{
Log($"=== Facial Motion 재접속 완료 ({reconnectedCount}/{facialMotionClients.Count}개) ===");
}
else
{
LogError("재접속에 성공한 클라이언트가 없습니다!");
}
}
#endregion
#region
/// <summary>
/// 일반 스크린샷 촬영
/// </summary>
public void CaptureScreenshot()
{
if (screenshotCamera == null)
{
LogError("촬영할 카메라가 설정되지 않았습니다!");
return;
}
string fileName = GenerateFileName("png");
string fullPath = Path.Combine(screenshotSavePath, fileName);
try
{
// 렌더 텍스처 생성
RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24);
RenderTexture currentRT = screenshotCamera.targetTexture;
// 카메라로 렌더링
screenshotCamera.targetTexture = rt;
screenshotCamera.Render();
// 텍스처를 Texture2D로 변환
RenderTexture.active = rt;
Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGB24, false);
screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0);
screenshot.Apply();
// PNG로 저장
byte[] bytes = screenshot.EncodeToPNG();
File.WriteAllBytes(fullPath, bytes);
// 정리
screenshotCamera.targetTexture = currentRT;
RenderTexture.active = null;
Destroy(rt);
Destroy(screenshot);
Log($"스크린샷 저장 완료: {fullPath}");
}
catch (Exception e)
{
LogError($"스크린샷 촬영 실패: {e.Message}");
}
}
/// <summary>
/// 알파 채널 포함 스크린샷 촬영
/// NiloToon Prepass 버퍼의 G 채널을 알파로 사용
/// </summary>
public void CaptureAlphaScreenshot()
{
if (screenshotCamera == null)
{
LogError("촬영할 카메라가 설정되지 않았습니다!");
return;
}
if (alphaShader == null)
{
LogError("알파 셰이더가 설정되지 않았습니다!");
return;
}
string fileName = GenerateFileName("png", "_Alpha");
string fullPath = Path.Combine(screenshotSavePath, fileName);
try
{
// 렌더 텍스처 생성
RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24);
RenderTexture currentRT = screenshotCamera.targetTexture;
// 카메라로 렌더링
screenshotCamera.targetTexture = rt;
screenshotCamera.Render();
// NiloToon Prepass 버퍼 가져오기
Texture niloToonPrepassBuffer = Shader.GetGlobalTexture(niloToonPrepassBufferName);
if (niloToonPrepassBuffer == null)
{
LogError($"NiloToon Prepass 버퍼를 찾을 수 없습니다: {niloToonPrepassBufferName}");
screenshotCamera.targetTexture = currentRT;
Destroy(rt);
return;
}
// 알파 합성용 머티리얼 생성
if (alphaMaterial == null)
{
alphaMaterial = new Material(alphaShader);
}
// 알파 채널 합성
RenderTexture alphaRT = new RenderTexture(screenshotWidth, screenshotHeight, 0, RenderTextureFormat.ARGB32);
alphaMaterial.SetTexture("_MainTex", rt);
alphaMaterial.SetTexture("_AlphaTex", niloToonPrepassBuffer);
alphaMaterial.SetFloat("_BlurRadius", alphaBlurRadius);
// Blit으로 알파 합성
Graphics.Blit(rt, alphaRT, alphaMaterial);
// 텍스처를 Texture2D로 변환
RenderTexture.active = alphaRT;
Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGBA32, false);
screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0);
screenshot.Apply();
// PNG로 저장
byte[] bytes = screenshot.EncodeToPNG();
File.WriteAllBytes(fullPath, bytes);
// 정리
screenshotCamera.targetTexture = currentRT;
RenderTexture.active = null;
Destroy(rt);
Destroy(alphaRT);
Destroy(screenshot);
Log($"알파 스크린샷 저장 완료: {fullPath}");
}
catch (Exception e)
{
LogError($"알파 스크린샷 촬영 실패: {e.Message}");
}
}
/// <summary>
/// 스크린샷 저장 폴더 열기
/// </summary>
public void OpenScreenshotFolder()
{
if (Directory.Exists(screenshotSavePath))
{
System.Diagnostics.Process.Start(screenshotSavePath);
Log($"저장 폴더 열기: {screenshotSavePath}");
}
else
{
LogError($"저장 폴더가 존재하지 않습니다: {screenshotSavePath}");
}
}
/// <summary>
/// 파일명 생성
/// </summary>
private string GenerateFileName(string extension, string suffix = "")
{
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
return $"{screenshotFilePrefix}{suffix}_{timestamp}.{extension}";
}
#endregion
#region (EasyMotion Recorder + OptiTrack Motive)
/// <summary>
/// 모션 녹화 시작 (EasyMotion Recorder + OptiTrack Motive)
/// </summary>
public void StartMotionRecording()
{
// OptiTrack 녹화 시작 (옵션이 켜져 있을 때만)
bool optitrackStarted = false;
if (recordOptiTrackWithMotion)
{
if (optitrackClient != null)
{
try
{
optitrackStarted = optitrackClient.StartRecording();
if (optitrackStarted)
{
Log("OptiTrack Motive 녹화 시작 성공");
}
else
{
LogError("OptiTrack Motive 녹화 시작 실패");
}
}
catch (System.Exception e)
{
LogError($"OptiTrack 녹화 시작 오류: {e.Message}");
}
}
else
{
Log("OptiTrack 클라이언트 없음 - OptiTrack 녹화 건너뜀");
}
}
else
{
Log("OptiTrack 녹화 옵션 꺼짐 - OptiTrack 녹화 건너뜀");
}
// EasyMotion Recorder 녹화 시작
int startedCount = 0;
if (motionRecorders != null && motionRecorders.Count > 0)
{
foreach (var recorder in motionRecorders)
{
if (recorder != null)
{
try
{
// RecordStart 메서드 호출
var method = recorder.GetType().GetMethod("RecordStart");
if (method != null)
{
method.Invoke(recorder, null);
startedCount++;
}
}
catch (System.Exception e)
{
LogError($"레코더 시작 실패 ({recorder.name}): {e.Message}");
}
}
}
if (startedCount > 0)
{
Log($"EasyMotion 녹화 시작 ({startedCount}/{motionRecorders.Count}개 레코더)");
}
else
{
LogError("녹화를 시작한 EasyMotion 레코더가 없습니다!");
}
}
else
{
Log("EasyMotion Recorder 없음 - EasyMotion 녹화 건너뜀");
}
// 하나라도 성공하면 녹화 중 상태로 설정
if (optitrackStarted || startedCount > 0)
{
isRecording = true;
Log($"=== 녹화 시작 완료 ===");
if (recordOptiTrackWithMotion)
{
Log($"OptiTrack: {(optitrackStarted ? "" : " ")}");
}
else
{
Log($"OptiTrack: 옵션 꺼짐");
}
Log($"EasyMotion: {startedCount}개 레코더 시작됨");
}
else
{
LogError("녹화를 시작할 수 있는 시스템이 없습니다!");
}
}
/// <summary>
/// 모션 녹화 중지 (EasyMotion Recorder + OptiTrack Motive)
/// </summary>
public void StopMotionRecording()
{
// OptiTrack 녹화 중지 (옵션이 켜져 있을 때만)
bool optitrackStopped = false;
if (recordOptiTrackWithMotion)
{
if (optitrackClient != null)
{
try
{
optitrackStopped = optitrackClient.StopRecording();
if (optitrackStopped)
{
Log("OptiTrack Motive 녹화 중지 성공");
}
else
{
LogError("OptiTrack Motive 녹화 중지 실패");
}
}
catch (System.Exception e)
{
LogError($"OptiTrack 녹화 중지 오류: {e.Message}");
}
}
else
{
Log("OptiTrack 클라이언트 없음 - OptiTrack 녹화 중지 건너뜀");
}
}
else
{
Log("OptiTrack 녹화 옵션 꺼짐 - OptiTrack 녹화 중지 건너뜀");
}
// EasyMotion Recorder 녹화 중지
int stoppedCount = 0;
if (motionRecorders != null && motionRecorders.Count > 0)
{
foreach (var recorder in motionRecorders)
{
if (recorder != null)
{
try
{
// RecordEnd 메서드 호출
var method = recorder.GetType().GetMethod("RecordEnd");
if (method != null)
{
method.Invoke(recorder, null);
stoppedCount++;
}
}
catch (System.Exception e)
{
LogError($"레코더 중지 실패 ({recorder.name}): {e.Message}");
}
}
}
if (stoppedCount > 0)
{
Log($"EasyMotion 녹화 중지 ({stoppedCount}/{motionRecorders.Count}개 레코더)");
}
else
{
LogError("녹화를 중지한 EasyMotion 레코더가 없습니다!");
}
}
else
{
Log("EasyMotion Recorder 없음 - EasyMotion 녹화 중지 건너뜀");
}
// 하나라도 성공하면 녹화 중지 상태로 설정
if (optitrackStopped || stoppedCount > 0)
{
isRecording = false;
Log($"=== 녹화 중지 완료 ===");
Log($"OptiTrack: {(optitrackStopped ? "" : " ")}");
Log($"EasyMotion: {stoppedCount}개 레코더 중지됨");
}
else
{
LogError("녹화를 중지할 수 있는 시스템이 없습니다!");
}
}
/// <summary>
/// 모션 녹화 토글 (시작/중지)
/// </summary>
public void ToggleMotionRecording()
{
if (isRecording)
{
StopMotionRecording();
}
else
{
StartMotionRecording();
}
}
/// <summary>
/// 현재 녹화 중인지 여부 반환
/// </summary>
public bool IsRecording()
{
return isRecording;
}
#endregion
/// <summary>
/// 명령어 실행 - WebSocket에서 받은 명령을 처리
/// </summary>
public void ExecuteCommand(string command, Dictionary<string, object> parameters)
{
Log($"명령어 실행: {command}");
switch (command)
{
// OptiTrack 마커
case "toggle_optitrack_markers":
ToggleOptitrackMarkers();
break;
case "show_optitrack_markers":
ShowOptitrackMarkers();
break;
case "hide_optitrack_markers":
HideOptitrackMarkers();
break;
// OptiTrack 재접속
case "reconnect_optitrack":
ReconnectOptitrack();
break;
// Facial Motion 재접속
case "reconnect_facial_motion":
ReconnectFacialMotion();
break;
case "refresh_facial_motion_clients":
RefreshFacialMotionClients();
break;
// EasyMotion Recorder
case "start_motion_recording":
StartMotionRecording();
break;
case "stop_motion_recording":
StopMotionRecording();
break;
case "toggle_motion_recording":
ToggleMotionRecording();
break;
case "refresh_motion_recorders":
RefreshMotionRecorders();
break;
// 스크린샷
case "capture_screenshot":
CaptureScreenshot();
break;
case "capture_alpha_screenshot":
CaptureAlphaScreenshot();
break;
case "open_screenshot_folder":
OpenScreenshotFolder();
break;
default:
LogError($"알 수 없는 명령어: {command}");
break;
}
}
private void OnDestroy()
{
if (alphaMaterial != null)
{
Destroy(alphaMaterial);
}
}
private void Log(string message)
{
if (enableDebugLog)
{
Debug.Log($"[SystemController] {message}");
}
}
private void LogError(string message)
{
Debug.LogError($"[SystemController] {message}");
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f8e9a1b2c3d4e5f6a7b8c9d0e1f2a3b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

BIN
Streamdeck/DEBUG_MARKER_BUTTON.md (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Streamdeck/DEPLOY_README.md (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Streamdeck/HOW_TO_DEBUG_STREAMDOCK.md (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Streamdeck/STREAMDOCK_SDK_REFERENCE.md (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Streamdeck/SYSTEM_CONTROLLER_GUIDE.md (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -69,36 +69,199 @@ function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inAction
case 'didReceiveSettings':
if (jsonObj.payload && jsonObj.context) {
const newSettings = jsonObj.payload.settings || {};
// actionType이 없으면 action UUID로 판단해서 강제 설정
if (!newSettings.actionType && jsonObj.action) {
console.log('⚠️ actionType 없음! action UUID로 재설정:', jsonObj.action);
if (jsonObj.action === 'com.mirabox.streamingle.optitrack_marker_toggle') {
newSettings.actionType = 'optitrack_marker_toggle';
console.log('✅ 마커 버튼으로 강제 설정됨');
// StreamDock SDK에 저장
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: jsonObj.context,
payload: newSettings
};
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 강제 설정 저장:', newSettings);
}
} else if (jsonObj.action === 'com.mirabox.streamingle.motion_recording_toggle') {
newSettings.actionType = 'motion_recording_toggle';
console.log('🎬 모션 녹화 버튼으로 강제 설정됨');
// StreamDock SDK에 저장
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: jsonObj.context,
payload: newSettings
};
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 강제 설정 저장:', newSettings);
}
} else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_reconnect') {
newSettings.actionType = 'optitrack_reconnect';
console.log('🔄 OptiTrack 재접속 버튼으로 강제 설정됨');
// StreamDock SDK에 저장
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: jsonObj.context,
payload: newSettings
};
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 강제 설정 저장:', newSettings);
}
} else if (jsonObj.action === 'com.mirabox.streamingle.facial_motion_reconnect') {
newSettings.actionType = 'facial_motion_reconnect';
console.log('😊 Facial Motion 재접속 버튼으로 강제 설정됨');
// StreamDock SDK에 저장
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: jsonObj.context,
payload: newSettings
};
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 강제 설정 저장:', newSettings);
}
} else if (jsonObj.action === 'com.mirabox.streamingle.screenshot') {
newSettings.actionType = 'screenshot';
console.log('📸 스크린샷 버튼으로 강제 설정됨');
// StreamDock SDK에 저장
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: jsonObj.context,
payload: newSettings
};
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 강제 설정 저장:', newSettings);
}
} else if (jsonObj.action === 'com.mirabox.streamingle.screenshot_alpha') {
newSettings.actionType = 'screenshot_alpha';
console.log('📷 알파 스크린샷 버튼으로 강제 설정됨');
// StreamDock SDK에 저장
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: jsonObj.context,
payload: newSettings
};
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 강제 설정 저장:', newSettings);
}
} else if (jsonObj.action === 'com.mirabox.streamingle.item') {
newSettings.actionType = 'item';
} else if (jsonObj.action === 'com.mirabox.streamingle.event') {
newSettings.actionType = 'event';
} else if (jsonObj.action === 'com.mirabox.streamingle.avatar_outfit') {
newSettings.actionType = 'avatar_outfit';
} else {
newSettings.actionType = 'camera';
}
}
buttonContexts.set(jsonObj.context, newSettings);
console.log('⚙️ 설정 업데이트:', newSettings);
updateButtonTitle(jsonObj.context);
}
break;
case 'willAppear':
console.log('👀 버튼 나타남:', jsonObj.context);
console.log('============================================');
console.log('👀 버튼 나타남 이벤트');
console.log('🔍 Context:', jsonObj.context);
console.log('🔍 Action UUID:', jsonObj.action);
let settings = jsonObj.payload?.settings || {};
console.log('⚙️ 초기 설정:', settings);
console.log('⚙️ 초기 설정:', JSON.stringify(settings));
// action UUID로 actionType 결정
if (jsonObj.action === 'com.mirabox.streamingle.item') {
settings.actionType = 'item';
console.log('🎯 아이템 컨트롤러 등록:', jsonObj.context);
console.log('🎯 아이템 컨트롤러 등록');
} else if (jsonObj.action === 'com.mirabox.streamingle.event') {
settings.actionType = 'event';
console.log('🎯 이벤트 컨트롤러 등록:', jsonObj.context);
console.log('🎯 이벤트 컨트롤러 등록');
} else if (jsonObj.action === 'com.mirabox.streamingle.avatar_outfit') {
settings.actionType = 'avatar_outfit';
console.log('👗 아바타 의상 컨트롤러 등록:', jsonObj.context);
console.log('👗 아바타 의상 컨트롤러 등록');
} else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_marker_toggle') {
settings.actionType = 'optitrack_marker_toggle';
console.log('✅✅✅ MARKER BUTTON DETECTED ✅✅✅');
console.log('✅ This is the OptiTrack Marker Toggle button!');
console.log('✅ When clicked, it should send: toggle_optitrack_markers');
// 기본 제목 설정
setButtonTitle(jsonObj.context, '마커\nON');
} else if (jsonObj.action === 'com.mirabox.streamingle.motion_recording_toggle') {
settings.actionType = 'motion_recording_toggle';
console.log('🎬🎬🎬 MOTION RECORDING BUTTON DETECTED 🎬🎬🎬');
console.log('🎬 This is the Motion Recording Toggle button!');
console.log('🎬 When clicked, it should send: toggle_motion_recording');
// 기본 제목 설정
setButtonTitle(jsonObj.context, '녹화\nOFF');
} else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_reconnect') {
settings.actionType = 'optitrack_reconnect';
console.log('🔄🔄🔄 OPTITRACK RECONNECT BUTTON DETECTED 🔄🔄🔄');
console.log('🔄 This is the OptiTrack Reconnect button!');
console.log('🔄 When clicked, it should send: reconnect_optitrack');
// 기본 제목 설정
setButtonTitle(jsonObj.context, '옵티트랙\n재접속');
} else if (jsonObj.action === 'com.mirabox.streamingle.facial_motion_reconnect') {
settings.actionType = 'facial_motion_reconnect';
console.log('😊😊😊 FACIAL MOTION RECONNECT BUTTON DETECTED 😊😊😊');
console.log('😊 This is the Facial Motion Reconnect button!');
console.log('😊 When clicked, it should send: reconnect_facial_motion');
// 기본 제목 설정
setButtonTitle(jsonObj.context, '페이셜\n재접속');
} else if (jsonObj.action === 'com.mirabox.streamingle.screenshot') {
settings.actionType = 'screenshot';
console.log('📸📸📸 SCREENSHOT BUTTON DETECTED 📸📸📸');
console.log('📸 This is the Screenshot button!');
console.log('📸 When clicked, it should send: capture_screenshot');
// 기본 제목 설정
setButtonTitle(jsonObj.context, '스크린샷');
} else if (jsonObj.action === 'com.mirabox.streamingle.screenshot_alpha') {
settings.actionType = 'screenshot_alpha';
console.log('📷📷📷 ALPHA SCREENSHOT BUTTON DETECTED 📷📷📷');
console.log('📷 This is the Alpha Screenshot button!');
console.log('📷 When clicked, it should send: capture_alpha_screenshot');
// 기본 제목 설정
setButtonTitle(jsonObj.context, '알파\n스크린샷');
} else {
settings.actionType = 'camera';
console.log('📹 카메라 컨트롤러 등록:', jsonObj.context);
console.log('📹 카메라 컨트롤러 등록 (기본값)');
console.log('📹 This button will send camera switch messages');
}
console.log('🎯 최종 actionType:', settings.actionType);
buttonContexts.set(jsonObj.context, settings);
console.log('💾 설정 저장됨:', settings);
// StreamDock SDK에도 설정 저장
if (websocket) {
const setSettingsMessage = {
event: 'setSettings',
context: jsonObj.context,
payload: settings
};
websocket.send(JSON.stringify(setSettingsMessage));
console.log('💾 StreamDock SDK에 설정 저장:', settings);
}
console.log('💾 설정 저장 완료');
console.log('============================================');
updateButtonTitle(jsonObj.context);
// Unity가 이미 연결되어 있다면 Property Inspector에 상태 전송
@ -140,7 +303,8 @@ function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inAction
case 'keyUp':
console.log('🔘 버튼 클릭됨!');
handleButtonClick(jsonObj.context);
console.log('🔍 Action UUID:', jsonObj.action);
handleButtonClick(jsonObj.context, jsonObj.action);
break;
case 'sendToPlugin':
@ -277,9 +441,10 @@ function connectToUnity() {
}
// 버튼 클릭 처리
function handleButtonClick(context) {
function handleButtonClick(context, actionUUID) {
console.log('🎯 버튼 클릭 처리 시작');
console.log('📍 컨텍스트:', context);
console.log('📍 Action UUID:', actionUUID);
console.log('🔌 Unity 연결 상태:', isUnityConnected);
if (!isUnityConnected || !unitySocket) {
@ -288,7 +453,7 @@ function handleButtonClick(context) {
// 연결 후 잠시 대기한 후 다시 시도
setTimeout(() => {
if (isUnityConnected && unitySocket) {
handleButtonClick(context);
handleButtonClick(context, actionUUID);
} else {
console.error('❌ Unity 재연결 실패');
}
@ -298,26 +463,108 @@ function handleButtonClick(context) {
// context별 settings 사용
const settings = getCurrentSettings(context);
const actionType = settings.actionType || 'camera'; // 기본값은 camera
let actionType = settings.actionType;
console.log('🎯 액션 타입:', actionType);
// actionType이 없으면 action UUID로 판단
if (!actionType && actionUUID) {
console.log('⚠️ actionType 없음! Action UUID로 결정:', actionUUID);
if (actionUUID === 'com.mirabox.streamingle.optitrack_marker_toggle') {
actionType = 'optitrack_marker_toggle';
console.log('✅ 마커 버튼으로 인식');
} else if (actionUUID === 'com.mirabox.streamingle.motion_recording_toggle') {
actionType = 'motion_recording_toggle';
console.log('🎬 모션 녹화 버튼으로 인식');
} else if (actionUUID === 'com.mirabox.streamingle.optitrack_reconnect') {
actionType = 'optitrack_reconnect';
console.log('🔄 OptiTrack 재접속 버튼으로 인식');
} else if (actionUUID === 'com.mirabox.streamingle.facial_motion_reconnect') {
actionType = 'facial_motion_reconnect';
console.log('😊 Facial Motion 재접속 버튼으로 인식');
} else if (actionUUID === 'com.mirabox.streamingle.screenshot') {
actionType = 'screenshot';
console.log('📸 스크린샷 버튼으로 인식');
} else if (actionUUID === 'com.mirabox.streamingle.screenshot_alpha') {
actionType = 'screenshot_alpha';
console.log('📷 알파 스크린샷 버튼으로 인식');
} else if (actionUUID === 'com.mirabox.streamingle.item') {
actionType = 'item';
} else if (actionUUID === 'com.mirabox.streamingle.event') {
actionType = 'event';
} else if (actionUUID === 'com.mirabox.streamingle.avatar_outfit') {
actionType = 'avatar_outfit';
} else {
actionType = 'camera';
}
// 설정에 저장
settings.actionType = actionType;
setCurrentSettings(context, settings);
}
if (!actionType) {
actionType = 'camera'; // 최종 폴백
}
console.log('╔════════════════════════════════════════╗');
console.log('║ BUTTON CLICKED - DEBUG INFO ║');
console.log('╠════════════════════════════════════════╣');
console.log(' Context:', context);
console.log(' Action UUID:', actionUUID);
console.log(' ActionType:', actionType);
console.log(' All Settings:', JSON.stringify(settings));
console.log('╚════════════════════════════════════════╝');
switch (actionType) {
case 'camera':
console.log('➡️ Routing to: CAMERA handler');
console.log(' Will send: {"type":"switch_camera","data":{...}}');
handleCameraAction(settings);
break;
case 'item':
console.log('➡️ Routing to: ITEM handler');
handleItemAction(settings);
break;
case 'event':
console.log('➡️ Routing to: EVENT handler');
handleEventAction(settings);
break;
case 'avatar_outfit':
console.log('➡️ Routing to: AVATAR OUTFIT handler');
handleAvatarOutfitAction(settings);
break;
case 'optitrack_marker_toggle':
console.log('➡️ Routing to: MARKER TOGGLE handler');
console.log(' Will send: {"type":"toggle_optitrack_markers"}');
handleOptitrackMarkerToggle(context);
break;
case 'motion_recording_toggle':
console.log('➡️ Routing to: MOTION RECORDING handler');
console.log(' Will send: {"type":"toggle_motion_recording"}');
handleMotionRecordingToggle(context);
break;
case 'optitrack_reconnect':
console.log('➡️ Routing to: OPTITRACK RECONNECT handler');
console.log(' Will send: {"type":"reconnect_optitrack"}');
handleOptitrackReconnect(context);
break;
case 'facial_motion_reconnect':
console.log('➡️ Routing to: FACIAL MOTION RECONNECT handler');
console.log(' Will send: {"type":"reconnect_facial_motion"}');
handleFacialMotionReconnect(context);
break;
case 'screenshot':
console.log('➡️ Routing to: SCREENSHOT handler');
console.log(' Will send: {"type":"capture_screenshot"}');
handleScreenshot(context);
break;
case 'screenshot_alpha':
console.log('➡️ Routing to: ALPHA SCREENSHOT handler');
console.log(' Will send: {"type":"capture_alpha_screenshot"}');
handleAlphaScreenshot(context);
break;
default:
console.log('⚠️ 알 수 없는 액션 타입:', actionType);
// 기본적으로 카메라 액션으로 처리
console.log('⚠️ WARNING: Unknown actionType:', actionType);
console.log(' Defaulting to CAMERA handler');
handleCameraAction(settings);
}
}
@ -477,6 +724,244 @@ function handleAvatarOutfitAction(settings) {
}
}
// OptiTrack 마커 토글 액션 처리
function handleOptitrackMarkerToggle(context) {
console.log('🎯 OptiTrack 마커 토글 실행');
// Unity에 마커 토글 요청
const message = JSON.stringify({
type: 'toggle_optitrack_markers'
});
console.log('📤 Unity에 OptiTrack 마커 토글 요청 전송:', message);
console.log('🔍 Unity 연결 상태:', isUnityConnected);
console.log('🔍 Unity 소켓 상태:', !!unitySocket);
if (unitySocket && unitySocket.readyState === WebSocket.OPEN) {
unitySocket.send(message);
console.log('✅ 메시지 전송 완료');
// 버튼 상태 토글 (0 <-> 1)
toggleButtonState(context);
} else {
console.error('❌ Unity 소켓이 연결되지 않음');
console.log('🔄 Unity 재연결 시도...');
connectToUnity();
}
}
// 버튼 상태 토글 함수
function toggleButtonState(context) {
const settings = getCurrentSettings(context);
const currentState = settings.currentState || 0;
const newState = currentState === 0 ? 1 : 0;
// 설정 업데이트
settings.currentState = newState;
setCurrentSettings(context, settings);
// 버튼 상태 변경
const stateMessage = {
event: 'setState',
context: context,
payload: {
state: newState,
target: 0 // hardware and software
}
};
if (websocket) {
websocket.send(JSON.stringify(stateMessage));
console.log('🎨 버튼 상태 업데이트:', newState === 0 ? 'ON' : 'OFF');
}
// 제목도 함께 변경
const title = newState === 0 ? '마커\nON' : '마커\nOFF';
setButtonTitle(context, title);
}
// 모션 녹화 토글 액션 처리
function handleMotionRecordingToggle(context) {
console.log('🎬 모션 녹화 토글 실행');
// Unity에 모션 녹화 토글 요청
const message = JSON.stringify({
type: 'toggle_motion_recording'
});
console.log('📤 Unity에 모션 녹화 토글 요청 전송:', message);
console.log('🔍 Unity 연결 상태:', isUnityConnected);
console.log('🔍 Unity 소켓 상태:', !!unitySocket);
if (unitySocket && unitySocket.readyState === WebSocket.OPEN) {
unitySocket.send(message);
console.log('✅ 메시지 전송 완료');
// 버튼 상태 토글 (0 <-> 1)
toggleMotionRecordingButtonState(context);
} else {
console.error('❌ Unity 소켓이 연결되지 않음');
console.log('🔄 Unity 재연결 시도...');
connectToUnity();
}
}
// 모션 녹화 버튼 상태 토글 함수
function toggleMotionRecordingButtonState(context) {
const settings = getCurrentSettings(context);
const currentState = settings.currentState || 0;
const newState = currentState === 0 ? 1 : 0;
// 설정 업데이트
settings.currentState = newState;
setCurrentSettings(context, settings);
// 버튼 상태 변경
const stateMessage = {
event: 'setState',
context: context,
payload: {
state: newState,
target: 0 // hardware and software
}
};
if (websocket) {
websocket.send(JSON.stringify(stateMessage));
console.log('🎨 녹화 버튼 상태 업데이트:', newState === 0 ? 'OFF' : 'REC');
}
// 제목도 함께 변경
const title = newState === 0 ? '녹화\nOFF' : '녹화\nREC';
setButtonTitle(context, title);
}
// OptiTrack 재접속 액션 처리
function handleOptitrackReconnect(context) {
console.log('🔄 OptiTrack 재접속 실행');
// Unity에 OptiTrack 재접속 요청
const message = JSON.stringify({
type: 'reconnect_optitrack'
});
console.log('📤 Unity에 OptiTrack 재접속 요청 전송:', message);
console.log('🔍 Unity 연결 상태:', isUnityConnected);
console.log('🔍 Unity 소켓 상태:', !!unitySocket);
if (unitySocket && unitySocket.readyState === WebSocket.OPEN) {
unitySocket.send(message);
console.log('✅ 메시지 전송 완료');
// 재접속은 상태가 없는 단순 액션이므로 버튼 상태 변경 없음
// 피드백을 위해 제목을 잠시 변경할 수 있음
setButtonTitle(context, '재접속\n중...');
// 1초 후 원래 제목으로 복구
setTimeout(() => {
setButtonTitle(context, '옵티트랙\n재접속');
}, 1000);
} else {
console.error('❌ Unity 소켓이 연결되지 않음');
console.log('🔄 Unity 재연결 시도...');
connectToUnity();
}
}
// Facial Motion 재접속 액션 처리
function handleFacialMotionReconnect(context) {
console.log('😊 Facial Motion 재접속 실행');
// Unity에 Facial Motion 재접속 요청
const message = JSON.stringify({
type: 'reconnect_facial_motion'
});
console.log('📤 Unity에 Facial Motion 재접속 요청 전송:', message);
console.log('🔍 Unity 연결 상태:', isUnityConnected);
console.log('🔍 Unity 소켓 상태:', !!unitySocket);
if (unitySocket && unitySocket.readyState === WebSocket.OPEN) {
unitySocket.send(message);
console.log('✅ 메시지 전송 완료');
// 재접속은 상태가 없는 단순 액션이므로 버튼 상태 변경 없음
// 피드백을 위해 제목을 잠시 변경할 수 있음
setButtonTitle(context, '재접속\n중...');
// 1초 후 원래 제목으로 복구
setTimeout(() => {
setButtonTitle(context, '페이셜\n재접속');
}, 1000);
} else {
console.error('❌ Unity 소켓이 연결되지 않음');
console.log('🔄 Unity 재연결 시도...');
connectToUnity();
}
}
// 스크린샷 액션 처리
function handleScreenshot(context) {
console.log('📸 스크린샷 촬영 실행');
// Unity에 스크린샷 요청
const message = JSON.stringify({
type: 'capture_screenshot'
});
console.log('📤 Unity에 스크린샷 요청 전송:', message);
console.log('🔍 Unity 연결 상태:', isUnityConnected);
console.log('🔍 Unity 소켓 상태:', !!unitySocket);
if (unitySocket && unitySocket.readyState === WebSocket.OPEN) {
unitySocket.send(message);
console.log('✅ 메시지 전송 완료');
// 피드백을 위해 제목을 잠시 변경
setButtonTitle(context, '촬영\n중...');
// 1초 후 원래 제목으로 복구
setTimeout(() => {
setButtonTitle(context, '스크린샷');
}, 1000);
} else {
console.error('❌ Unity 소켓이 연결되지 않음');
console.log('🔄 Unity 재연결 시도...');
connectToUnity();
}
}
// 알파 스크린샷 액션 처리
function handleAlphaScreenshot(context) {
console.log('📷 알파 스크린샷 촬영 실행');
// Unity에 알파 스크린샷 요청
const message = JSON.stringify({
type: 'capture_alpha_screenshot'
});
console.log('📤 Unity에 알파 스크린샷 요청 전송:', message);
console.log('🔍 Unity 연결 상태:', isUnityConnected);
console.log('🔍 Unity 소켓 상태:', !!unitySocket);
if (unitySocket && unitySocket.readyState === WebSocket.OPEN) {
unitySocket.send(message);
console.log('✅ 메시지 전송 완료');
// 피드백을 위해 제목을 잠시 변경
setButtonTitle(context, '촬영\n중...');
// 1초 후 원래 제목으로 복구
setTimeout(() => {
setButtonTitle(context, '알파\n스크린샷');
}, 1000);
} else {
console.error('❌ Unity 소켓이 연결되지 않음');
console.log('🔄 Unity 재연결 시도...');
connectToUnity();
}
}
// Property Inspector 메시지 처리
function handlePropertyInspectorMessage(payload, context, actionUUID) {
const command = payload.command;
@ -1243,6 +1728,28 @@ function handleUnityMessage(data) {
}
}
// 버튼 제목을 직접 설정하는 함수
function setButtonTitle(context, title) {
if (!websocket || !context) {
console.log('🚫 WebSocket 또는 context 없음 - 제목 설정 건너뜀');
return;
}
console.log(`🏷️ 버튼 제목 설정: "${title}" (Context: ${context})`);
const message = {
event: 'setTitle',
context: context,
payload: {
title: title,
target: 0 // hardware and software
}
};
websocket.send(JSON.stringify(message));
console.log('✅ setTitle 메시지 전송 완료');
}
// 모든 버튼의 제목 업데이트
function updateAllButtonTitles() {
for (const context of buttonContexts.keys()) {
@ -1408,6 +1915,38 @@ function updateButtonTitle(context) {
console.log('👗 아바타 목록이 없거나 인덱스가 범위를 벗어남');
console.log('👗 목록 길이:', avatarOutfitList ? avatarOutfitList.length : 'null');
}
} else if (actionType === 'optitrack_marker_toggle') {
// OptiTrack 마커 토글 버튼
const currentState = settings.currentState || 0;
title = currentState === 0 ? '마커\nON' : '마커\nOFF';
isActive = true; // 항상 활성 상태
console.log('🎯 OptiTrack 마커 버튼 제목:', title, '(State:', currentState, ')');
} else if (actionType === 'motion_recording_toggle') {
// 모션 녹화 토글 버튼
const currentState = settings.currentState || 0;
title = currentState === 0 ? '녹화\nOFF' : '녹화\nREC';
isActive = true; // 항상 활성 상태
console.log('🎬 모션 녹화 버튼 제목:', title, '(State:', currentState, ')');
} else if (actionType === 'optitrack_reconnect') {
// OptiTrack 재접속 버튼
title = '옵티트랙\n재접속';
isActive = true; // 항상 활성 상태
console.log('🔄 OptiTrack 재접속 버튼 제목:', title);
} else if (actionType === 'facial_motion_reconnect') {
// Facial Motion 재접속 버튼
title = '페이셜\n재접속';
isActive = true; // 항상 활성 상태
console.log('😊 Facial Motion 재접속 버튼 제목:', title);
} else if (actionType === 'screenshot') {
// 스크린샷 버튼
title = '스크린샷';
isActive = true; // 항상 활성 상태
console.log('📸 스크린샷 버튼 제목:', title);
} else if (actionType === 'screenshot_alpha') {
// 알파 스크린샷 버튼
title = '알파\n스크린샷';
isActive = true; // 항상 활성 상태
console.log('📷 알파 스크린샷 버튼 제목:', title);
}
// StreamDock에 제목 업데이트 요청

View File

@ -0,0 +1,39 @@
@echo off
chcp 65001 >nul
:: Check for admin rights
net session >nul 2>&1
if %errorLevel% == 0 (
goto :run_script
) else (
echo Requesting administrator privileges...
echo.
:: Restart as administrator
powershell -Command "Start-Process '%~f0' -Verb RunAs"
exit /b
)
:run_script
echo.
echo ========================================
echo StreamDeck Plugin Auto Deploy
echo ========================================
echo.
echo Running PowerShell deployment script...
echo.
REM Run PowerShell script with admin rights
powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dp0deploy-plugin.ps1'"
if %ERRORLEVEL% NEQ 0 (
echo.
echo ERROR: Deployment failed!
echo.
pause
exit /b 1
)
echo.
echo Complete! Window will close in 2 seconds...
timeout /t 2 /nobreak >nul

View File

@ -0,0 +1,166 @@
# StreamDeck Plugin Auto Deployment Script
# Usage: Run .\deploy-plugin.ps1 in PowerShell
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " StreamDeck Plugin Auto Deploy" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Path settings
$PluginSourcePath = "$PSScriptRoot\com.mirabox.streamingle.sdPlugin"
$PluginDestPath = "$env:APPDATA\Hotspot\StreamDock\plugins\com.mirabox.streamingle.sdPlugin"
$StreamDeckExe = "C:\Program Files\Hotspot\StreamDock\StreamDock.exe"
# Check StreamDock executable path
$StreamDeckExeFound = $false
if (-not (Test-Path $StreamDeckExe)) {
Write-Host "WARNING: StreamDock executable not found: $StreamDeckExe" -ForegroundColor Yellow
Write-Host "Checking alternative paths..." -ForegroundColor Yellow
# Check alternative paths
$AlternativePaths = @(
"C:\Program Files (x86)\Hotspot\StreamDock\StreamDock.exe",
"C:\Program Files\Elgato\StreamDeck\StreamDeck.exe",
"$env:ProgramFiles\Hotspot\StreamDock\StreamDock.exe",
"${env:ProgramFiles(x86)}\Hotspot\StreamDock\StreamDock.exe",
"$env:LOCALAPPDATA\Hotspot\StreamDock\StreamDock.exe",
"$env:APPDATA\Hotspot\StreamDock\StreamDock.exe"
)
foreach ($path in $AlternativePaths) {
if (Test-Path $path) {
$StreamDeckExe = $path
$StreamDeckExeFound = $true
Write-Host "SUCCESS: StreamDock executable found: $StreamDeckExe" -ForegroundColor Green
break
}
}
if (-not $StreamDeckExeFound) {
Write-Host "WARNING: StreamDock executable not found!" -ForegroundColor Yellow
Write-Host "Plugin files will be copied. Please restart StreamDock manually." -ForegroundColor Yellow
}
} else {
$StreamDeckExeFound = $true
}
# Step 1: Stop StreamDock process
Write-Host "Step 1: Stopping StreamDock process..." -ForegroundColor Yellow
$StreamDeckProcess = Get-Process -Name "StreamDock" -ErrorAction SilentlyContinue
if ($StreamDeckProcess) {
Write-Host " StreamDock process found (PID: $($StreamDeckProcess.Id))" -ForegroundColor Gray
try {
Stop-Process -Name "StreamDock" -Force -ErrorAction Stop
Start-Sleep -Seconds 2
Write-Host " SUCCESS: StreamDock process stopped" -ForegroundColor Green
} catch {
Write-Host " WARNING: Failed to stop process (permission issue)" -ForegroundColor Yellow
Write-Host " Continuing... Please close StreamDock manually." -ForegroundColor Yellow
Write-Host "" -ForegroundColor Yellow
Write-Host " => Right-click StreamDock icon in taskbar -> Exit" -ForegroundColor Cyan
Write-Host " => Press any key to continue after closing..." -ForegroundColor Cyan
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Write-Host ""
}
} else {
Write-Host " INFO: StreamDock process is not running" -ForegroundColor Gray
}
Write-Host ""
# Step 2: Backup and remove existing plugin folder
Write-Host "Step 2: Cleaning up existing plugin folder..." -ForegroundColor Yellow
if (Test-Path $PluginDestPath) {
# Create backup folder
$BackupPath = "$PluginDestPath.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
Write-Host " Backing up existing plugin: $BackupPath" -ForegroundColor Gray
try {
Move-Item -Path $PluginDestPath -Destination $BackupPath -Force
Write-Host " SUCCESS: Existing plugin backed up" -ForegroundColor Green
} catch {
Write-Host " WARNING: Backup failed, attempting to delete..." -ForegroundColor Yellow
Remove-Item -Path $PluginDestPath -Recurse -Force -ErrorAction SilentlyContinue
}
} else {
Write-Host " INFO: No existing plugin found" -ForegroundColor Gray
}
# Ensure parent directory exists
$PluginDestParent = Split-Path -Parent $PluginDestPath
if (-not (Test-Path $PluginDestParent)) {
Write-Host " Creating plugin directory: $PluginDestParent" -ForegroundColor Gray
New-Item -ItemType Directory -Path $PluginDestParent -Force | Out-Null
}
Write-Host ""
# Step 3: Copy new plugin
Write-Host "Step 3: Copying new plugin..." -ForegroundColor Yellow
if (-not (Test-Path $PluginSourcePath)) {
Write-Host " ERROR: Source plugin folder not found: $PluginSourcePath" -ForegroundColor Red
exit 1
}
try {
Write-Host " Copying from: $PluginSourcePath" -ForegroundColor Gray
Write-Host " Copying to: $PluginDestPath" -ForegroundColor Gray
Copy-Item -Path $PluginSourcePath -Destination $PluginDestPath -Recurse -Force
# Count copied files
$CopiedFiles = Get-ChildItem -Path $PluginDestPath -Recurse -File
Write-Host " SUCCESS: Plugin copied ($($CopiedFiles.Count) files)" -ForegroundColor Green
} catch {
Write-Host " ERROR: Plugin copy failed: $_" -ForegroundColor Red
exit 1
}
Write-Host ""
# Step 4: Restart StreamDock
Write-Host "Step 4: Restarting StreamDock..." -ForegroundColor Yellow
if ($StreamDeckExeFound) {
try {
Start-Process -FilePath $StreamDeckExe
Write-Host " StreamDock started: $StreamDeckExe" -ForegroundColor Gray
# Wait for process to start
Start-Sleep -Seconds 3
$NewProcess = Get-Process -Name "StreamDock" -ErrorAction SilentlyContinue
if ($NewProcess) {
Write-Host " SUCCESS: StreamDock restarted (PID: $($NewProcess.Id))" -ForegroundColor Green
} else {
Write-Host " WARNING: Cannot verify StreamDock process (may be running in background)" -ForegroundColor Yellow
}
} catch {
Write-Host " ERROR: StreamDock restart failed: $_" -ForegroundColor Red
Write-Host " Please start StreamDock manually." -ForegroundColor Yellow
}
} else {
Write-Host " WARNING: StreamDock executable not found, skipping auto-restart." -ForegroundColor Yellow
Write-Host " Please restart StreamDock manually:" -ForegroundColor Yellow
Write-Host " 1. Right-click StreamDock icon in taskbar -> Exit" -ForegroundColor Gray
Write-Host " 2. Search 'StreamDock' in Start Menu and run" -ForegroundColor Gray
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Deployment Complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Plugin has been updated." -ForegroundColor Green
Write-Host "Check new actions in StreamDeck!" -ForegroundColor Green
Write-Host ""
# Auto-close after 5 seconds
Write-Host "Auto-closing in 5 seconds..." -ForegroundColor Gray
Start-Sleep -Seconds 5

45
Streamdeck/view-logs.bat Normal file
View File

@ -0,0 +1,45 @@
@echo off
chcp 65001 >nul
echo ========================================
echo StreamDock Plugin Log Viewer
echo ========================================
echo.
echo Opening StreamDock log directory...
echo.
set LOG_DIR=%APPDATA%\Hotspot\StreamDock\logs
if exist "%LOG_DIR%" (
echo Log directory found: %LOG_DIR%
echo.
echo Recent log files:
dir /B /O-D "%LOG_DIR%\*.log" 2>nul | findstr /N "^"
echo.
echo Opening log directory in Explorer...
explorer "%LOG_DIR%"
echo.
echo Opening latest log file in Notepad...
for /f "delims=" %%i in ('dir /B /O-D "%LOG_DIR%\*.log" 2^>nul') do (
start notepad "%LOG_DIR%\%%i"
goto :done
)
) else (
echo WARNING: Log directory not found!
echo Checking alternative locations...
echo.
set ALT_LOG=%LOCALAPPDATA%\Hotspot\StreamDock\logs
if exist "!ALT_LOG!" (
echo Found at: !ALT_LOG!
explorer "!ALT_LOG!"
) else (
echo No logs found. StreamDock may not have been launched yet.
)
)
:done
echo.
echo Press any key to exit...
pause >nul