576 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

console.log('👗 Avatar Outfit Property Inspector 로드됨');
// 화면에 로그를 표시하는 함수
function logToScreen(msg, color = "#00ff00") {
let logDiv = document.getElementById('logArea');
if (!logDiv) {
return; // 로그 영역이 아직 없으면 무시
}
const line = document.createElement('div');
line.style.color = color;
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
logDiv.appendChild(line);
logDiv.scrollTop = logDiv.scrollHeight;
// 로그가 너무 많아지면 오래된 것 제거 (최대 100줄)
if (logDiv.children.length > 100) {
logDiv.removeChild(logDiv.firstChild);
}
}
// 기존 console.log/console.error를 화면에도 출력
const origLog = console.log;
console.log = function(...args) {
origLog.apply(console, args);
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : a)).join(' ');
logToScreen(msg, '#00ff00');
};
const origError = console.error;
console.error = function(...args) {
origError.apply(console, args);
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : a)).join(' ');
logToScreen(msg, '#ff5555');
};
const origWarn = console.warn;
console.warn = function(...args) {
origWarn.apply(console, args);
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : a)).join(' ');
logToScreen(msg, '#ffaa00');
};
// 전역 변수
let websocket = null;
let uuid = null; // Property Inspector UUID
let actionContext = null; // 버튼별 고유 컨텍스트
let actionInfo = null;
let avatarList = [];
let isUnityConnected = false;
let settings = {};
// Stream Deck 연결
function connectElgatoStreamDeckSocket(inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo) {
console.log('🔌 Avatar Outfit Property Inspector 연결 중...');
console.log('📡 포트:', inPort, 'UUID:', inPropertyInspectorUUID);
uuid = inPropertyInspectorUUID;
// Parse action info - 버튼별 고유 컨텍스트와 설정 추출
try {
if (inActionInfo) {
console.log('🔍 Raw actionInfo:', inActionInfo);
actionInfo = JSON.parse(inActionInfo);
actionContext = actionInfo.context; // 버튼별 고유 컨텍스트
settings = actionInfo.payload?.settings || {};
// actionType이 없으면 강제로 설정
if (!settings.actionType) {
settings.actionType = 'avatar_outfit';
console.log('🔧 actionType이 없어서 강제로 설정:', settings.actionType);
}
console.log(' Property Inspector UUID:', uuid);
console.log(' 버튼 컨텍스트:', actionContext);
console.log(' 초기 설정:', settings);
console.log(' actionInfo 전체:', actionInfo);
} else {
console.error(' actionInfo가 없습니다!');
}
} catch (error) {
console.error(' ActionInfo 파싱 오류:', error);
console.log('🔍 파싱 시도한 문자열:', inActionInfo);
}
websocket = new WebSocket('ws://localhost:' + inPort);
websocket.onopen = function() {
console.log('✅ Property Inspector 연결됨');
// StreamDeck에 등록 (카메라와 동일한 패턴)
websocket.send(JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID
}));
// Unity 상태 요청 (카메라와 동일한 패턴)
setTimeout(() => {
console.log('🔍 Plugin에 디버그 메시지 전송 시작');
// actionType이 설정되었으면 즉시 Plugin에 저장
if (settings.actionType === 'avatar_outfit') {
const actionTypeMessage = {
action: 'com.mirabox.streamingle.avatar_outfit',
event: 'setSettings',
context: uuid,
payload: settings
};
websocket.send(JSON.stringify(actionTypeMessage));
console.log('🔧 초기 actionType 설정을 Plugin에 저장:', settings);
}
// 먼저 Plugin이 이 버튼을 인식하는지 확인
sendToPlugin('debug_test', {
message: 'Avatar outfit controller test',
context: actionContext,
action: 'com.mirabox.streamingle.avatar_outfit'
});
// Unity 상태 요청
sendToPlugin('get_unity_status');
}, 500);
// 초기 설정 로드
loadInitialSettings();
};
websocket.onmessage = function(evt) {
try {
const jsonObj = JSON.parse(evt.data);
handleMessage(jsonObj);
} catch (error) {
console.error('❌ 메시지 파싱 오류:', error);
}
};
websocket.onclose = function() {
console.log('❌ Property Inspector 연결 끊어짐');
websocket = null;
// 연결 상태를 비연결로 표시
handleUnityConnected(false);
};
websocket.onerror = function(error) {
console.error('❌ Property Inspector 연결 오류:', error);
websocket = null;
// 연결 상태를 비연결로 표시
handleUnityConnected(false);
};
}
// 메시지 처리
function handleMessage(jsonObj) {
console.log('📨 메시지 수신:', jsonObj.event);
console.log('📨 전체 메시지:', JSON.stringify(jsonObj, null, 2));
// sendToPropertyInspector 이벤트 처리 (카메라 방식과 동일)
if (jsonObj.event === 'sendToPropertyInspector' && jsonObj.payload && jsonObj.payload.event) {
const innerEvent = jsonObj.payload.event;
console.log('📨 내부 이벤트:', innerEvent, 'payload:', jsonObj.payload);
switch (innerEvent) {
case 'unity_connected':
console.log('✅ Unity 연결 상태 업데이트');
handleUnityConnected(jsonObj.payload.connected);
break;
case 'avatar_outfit_list':
console.log('👗 아바타 의상 목록 수신');
handleAvatarOutfitList(jsonObj.payload);
break;
case 'avatar_outfit_changed':
console.log('🎭 아바타 의상 변경 알림');
handleAvatarOutfitChanged(jsonObj.payload);
break;
case 'camera_list':
console.log('⚠️ 카메라 데이터를 받았지만 아바타 컨트롤러에서는 무시함');
break;
case 'item_list':
console.log('⚠️ 아이템 데이터를 받았지만 아바타 컨트롤러에서는 무시함');
break;
case 'event_list':
console.log('⚠️ 이벤트 데이터를 받았지만 아바타 컨트롤러에서는 무시함');
break;
case 'debug_response':
console.log('✅ Plugin으로부터 디버그 응답 받음!');
console.log('📨 응답 데이터:', jsonObj.payload);
break;
default:
console.log('❓ 알 수 없는 내부 이벤트:', innerEvent);
break;
}
return;
}
switch(jsonObj.event) {
case 'sendToPropertyInspector':
if (jsonObj.payload) {
handlePluginMessage(jsonObj.payload);
}
break;
case 'didReceiveSettings':
if (jsonObj.payload && jsonObj.payload.settings) {
updateUIFromSettings(jsonObj.payload.settings);
}
break;
// 기존 방식도 유지 (호환성)
case 'unity_connected':
console.log('✅ Unity 연결 상태 업데이트 (기존 방식)');
handleUnityConnected(jsonObj.payload?.connected);
break;
case 'avatar_outfit_list':
console.log('👗 아바타 의상 목록 수신 (기존 방식)');
handleAvatarOutfitList(jsonObj.payload);
break;
case 'avatar_outfit_changed':
console.log('🎭 아바타 의상 변경 알림 (기존 방식)');
handleAvatarOutfitChanged(jsonObj.payload);
break;
}
}
// 플러그인 메시지 처리
function handlePluginMessage(payload) {
console.log('📨 플러그인 메시지:', payload.event, payload);
switch(payload.event) {
case 'unity_connected':
console.log('🔗 Unity 연결 이벤트 수신:', payload);
handleUnityConnected(payload.connected);
break;
case 'avatar_outfit_list':
console.log('👗 아바타 의상 목록 이벤트 수신:', payload);
handleAvatarOutfitList(payload);
break;
case 'avatar_outfit_changed':
console.log('🎭 아바타 의상 변경 이벤트 수신:', payload);
handleAvatarOutfitChanged(payload);
break;
default:
console.log('❓ 알 수 없는 이벤트:', payload.event);
break;
}
}
// Unity 연결 상태 처리
function handleUnityConnected(connected) {
console.log('🔗 Unity 연결 상태:', connected);
isUnityConnected = connected;
const statusIndicator = document.getElementById('connection-status');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const connectionDetail = document.getElementById('connection-detail');
const avatarSelect = document.getElementById('avatar-select');
const refreshButton = document.getElementById('refresh-button');
if (connected) {
statusIndicator.classList.add('connected');
statusDot.classList.add('connected');
statusText.textContent = 'Unity 서버 연결됨';
connectionDetail.textContent = '아바타 데이터를 불러오는 중...';
avatarSelect.disabled = false;
refreshButton.disabled = false;
avatarSelect.innerHTML = '<option value="">아바타 목록을 불러오는 중...</option>';
// 연결 즉시 아바타 목록 요청
console.log('🔄 Unity 연결됨 - 아바타 목록 요청');
setTimeout(() => {
refreshAvatarList();
}, 500);
} else {
statusIndicator.classList.remove('connected');
statusDot.classList.remove('connected');
statusText.textContent = 'Unity 서버 연결 안됨';
connectionDetail.textContent = 'Unity를 실행하고 AvatarOutfitController가 있는지 확인하세요';
avatarSelect.disabled = true;
refreshButton.disabled = true;
avatarSelect.innerHTML = '<option value="">Unity가 연결되지 않음</option>';
updateOutfitSelect(-1);
}
}
// 아바타 의상 목록 처리
function handleAvatarOutfitList(payload) {
console.log('👗 아바타 의상 목록 수신:', payload);
console.log('👗 payload.avatars:', payload.avatars);
console.log('👗 Array.isArray(payload.avatars):', Array.isArray(payload.avatars));
if (payload.avatars && Array.isArray(payload.avatars)) {
avatarList = payload.avatars;
console.log('👗 아바타 목록 저장됨:', avatarList.length, '개');
console.log('👗 아바타 목록 상세:', avatarList);
updateAvatarSelect();
// 현재 설정된 아바타 확인
const avatarSelect = document.getElementById('avatar-select');
if (avatarSelect.value !== '') {
const selectedIndex = parseInt(avatarSelect.value);
if (!isNaN(selectedIndex)) {
updateOutfitSelect(selectedIndex);
}
}
} else {
console.log('❌ 아바타 데이터가 올바르지 않음:', payload.avatars);
avatarList = [];
updateAvatarSelect();
}
updateCurrentStatus(payload.currentIndex);
}
// 아바타 의상 변경 처리
function handleAvatarOutfitChanged(payload) {
console.log('🎭 아바타 의상 변경:', payload);
handleAvatarOutfitList(payload); // 같은 방식으로 처리
}
// 아바타 선택 업데이트
function updateAvatarSelect() {
const avatarSelect = document.getElementById('avatar-select');
const connectionDetail = document.getElementById('connection-detail');
avatarSelect.innerHTML = '';
if (avatarList.length === 0) {
avatarSelect.innerHTML = '<option value="">등록된 아바타가 없음</option>';
connectionDetail.textContent = 'Unity에서 AvatarOutfitController에 아바타를 추가하세요';
return;
}
avatarSelect.innerHTML = '<option value="">아바타를 선택하세요...</option>';
avatarList.forEach((avatar, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${avatar.name} (${avatar.outfits ? avatar.outfits.length : 0}개 의상)`;
avatarSelect.appendChild(option);
});
connectionDetail.textContent = `${avatarList.length}개의 아바타를 발견했습니다`;
}
// 의상 선택 업데이트
function updateOutfitSelect(avatarIndex) {
const outfitSelect = document.getElementById('outfit-select');
outfitSelect.innerHTML = '';
if (avatarIndex < 0 || avatarIndex >= avatarList.length) {
outfitSelect.innerHTML = '<option value="">먼저 아바타를 선택하세요</option>';
outfitSelect.disabled = true;
return;
}
const avatar = avatarList[avatarIndex];
if (!avatar.outfits || avatar.outfits.length === 0) {
outfitSelect.innerHTML = '<option value="">등록된 의상이 없음</option>';
outfitSelect.disabled = true;
return;
}
// 기본 선택 옵션 추가
outfitSelect.innerHTML = '<option value="">의상을 선택하세요...</option>';
// 의상 목록 추가
avatar.outfits.forEach((outfit, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = outfit.name;
outfitSelect.appendChild(option);
});
outfitSelect.disabled = false;
// 기본값 설정 (첫 번째 의상)
if (avatar.outfits.length > 0) {
outfitSelect.value = 0;
}
}
// 현재 상태 업데이트
function updateCurrentStatus(currentAvatarIndex) {
const currentAvatar = document.getElementById('current-avatar');
const currentOutfit = document.getElementById('current-outfit');
if (currentAvatarIndex >= 0 && currentAvatarIndex < avatarList.length) {
const avatar = avatarList[currentAvatarIndex];
currentAvatar.textContent = `현재: ${avatar.name}`;
currentOutfit.textContent = `현재 의상: ${avatar.current_outfit_name || '정보 없음'}`;
} else {
currentAvatar.textContent = '현재: 선택되지 않음';
currentOutfit.textContent = '현재 의상: 정보 없음';
}
}
// Send message to plugin
function sendToPlugin(command, data = {}) {
console.log('🚀 sendToPlugin 호출:', command, data);
console.log('🔍 WebSocket 상태:', websocket ? 'available' : 'null');
console.log('🔍 actionContext 상태:', actionContext);
if (!websocket) {
console.error('❌ WebSocket not available');
return;
}
if (!uuid) {
console.error('❌ UUID not available');
return;
}
try {
const message = {
action: 'com.mirabox.streamingle.avatar_outfit', // manifest.json의 Action UUID
event: 'sendToPlugin',
context: uuid, // Property Inspector UUID (Stream Deck 표준)
payload: {
command,
buttonContext: actionContext, // 버튼의 실제 context 정보도 함께 전송
...data
}
};
websocket.send(JSON.stringify(message));
console.log('📤 Message sent to plugin:', command, data, 'context:', uuid);
} catch (error) {
console.error('❌ Failed to send message to plugin:', error);
}
}
// Unity 상태 요청
function requestUnityStatus() {
sendToPlugin('get_unity_status');
}
// 아바타 목록 새로고침
function refreshAvatarList() {
sendToPlugin('refresh_avatar_outfit_list');
}
// 설정 저장
function saveSettings() {
if (!uuid) {
console.error('❌ No uuid available for saving settings');
return;
}
if (!websocket) {
console.log('⚠️ WebSocket이 연결되지 않음, 설정 저장 건너뜀');
return;
}
const avatarSelect = document.getElementById('avatar-select');
const outfitSelect = document.getElementById('outfit-select');
const newSettings = {
avatarIndex: parseInt(avatarSelect.value || '0'),
outfitIndex: parseInt(outfitSelect.value || '0'),
actionType: 'avatar_outfit' // 🔥 중요: actionType 추가!
};
// 전역 설정도 업데이트
settings = { ...settings, ...newSettings };
const message = {
action: 'com.mirabox.streamingle.avatar_outfit',
event: 'setSettings',
context: uuid, // Property Inspector UUID 사용 (Stream Deck 표준)
payload: newSettings
};
try {
websocket.send(JSON.stringify(message));
console.log('📤 설정 저장됨 - Context:', uuid, 'Settings:', newSettings);
// 설정이 저장되면 Plugin에게 아바타 의상 변경 요청도 보냄
sendToPlugin('set_avatar_outfit', {
avatarIndex: newSettings.avatarIndex,
outfitIndex: newSettings.outfitIndex
});
// 버튼 제목 업데이트 요청
sendToPlugin('update_title', {
avatarIndex: newSettings.avatarIndex,
outfitIndex: newSettings.outfitIndex
});
} catch (error) {
console.error('❌ 설정 저장 실패:', error);
websocket = null;
handleUnityConnected(false);
}
}
// 초기 설정 로드
function loadInitialSettings() {
console.log('🔧 초기 설정 로드 시작');
console.log('🔧 현재 settings:', settings);
// 초기화가 완료된 후 UI 업데이트
setTimeout(() => {
updateUIFromSettings(settings);
}, 100);
}
// 설정에서 UI 업데이트
function updateUIFromSettings(settingsToApply) {
console.log('🎨 UI 업데이트 시작:', settingsToApply);
const avatarSelect = document.getElementById('avatar-select');
const outfitSelect = document.getElementById('outfit-select');
if (settingsToApply.avatarIndex !== undefined && avatarSelect) {
console.log('👤 아바타 인덱스 적용:', settingsToApply.avatarIndex);
avatarSelect.value = settingsToApply.avatarIndex;
updateOutfitSelect(settingsToApply.avatarIndex);
}
if (settingsToApply.outfitIndex !== undefined && outfitSelect) {
console.log('👗 의상 인덱스 적용:', settingsToApply.outfitIndex);
outfitSelect.value = settingsToApply.outfitIndex;
}
console.log('✅ UI 업데이트 완료');
}
// 이벤트 리스너
document.addEventListener('DOMContentLoaded', function() {
console.log('👗 Avatar Outfit Property Inspector DOM 준비됨');
// 아바타 선택 변경
const avatarSelect = document.getElementById('avatar-select');
if (avatarSelect) {
avatarSelect.addEventListener('change', function() {
const selectedIndex = parseInt(this.value);
if (!isNaN(selectedIndex)) {
updateOutfitSelect(selectedIndex);
} else {
updateOutfitSelect(-1);
}
saveSettings();
});
}
// 의상 선택 변경
const outfitSelect = document.getElementById('outfit-select');
if (outfitSelect) {
outfitSelect.addEventListener('change', saveSettings);
}
// 새로고침 버튼
const refreshButton = document.getElementById('refresh-button');
if (refreshButton) {
refreshButton.addEventListener('click', function() {
console.log('🔄 새로고침 버튼 클릭됨');
// Unity 상태를 다시 확인하고 아바타 목록도 새로고침
requestUnityStatus();
refreshAvatarList();
// 강제로 연결 상태를 true로 설정 (다른 컨트롤러가 연결되어 있다면 Unity는 작동 중)
setTimeout(() => {
console.log('🔧 새로고침 후 강제로 Unity 연결 상태를 true로 설정');
handleUnityConnected(true);
}, 1000);
});
}
});
console.log('✅ Avatar Outfit Property Inspector 스크립트 로드 완료');