576 lines
21 KiB
JavaScript
576 lines
21 KiB
JavaScript
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 스크립트 로드 완료'); |