Created
August 2, 2025 13:59
-
-
Save gitcrtn/35df0ac69d3ae8ea06a6f8fca5c345be to your computer and use it in GitHub Desktop.
VRC Avatar Pedestal Auto Placer for Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| How to use: | |
| 1. Open your "Favorite Avatars" page from VRChat Site on Chrome/Edge browser. | |
| 2. Switch to List View. | |
| 3. Run DumpFavAvatarsSnippet.js as snippet from devtools. | |
| 4. Copy json from console of devtools. | |
| 5. Put AvatarPedestalAutoPlacer.cs in Assets/Editor/ of your unity project. | |
| 6. Run "Tools/Avatar Pedestal Auto Placer" from Unity. | |
| 7. Paste json to textbox of Portal Auto Placer. | |
| 8. Click "Load JSON Text" button. | |
| 9. Click "Create Avatar Pedestals" button. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using UnityEngine; | |
| using UnityEditor; | |
| using System.Collections.Generic; | |
| using VRC.SDK3.Components; | |
| namespace VRChatTools | |
| { | |
| [System.Serializable] | |
| public class AvatarData | |
| { | |
| public string name; | |
| public string id; | |
| } | |
| [System.Serializable] | |
| public class AvatarDataList | |
| { | |
| public List<AvatarData> avatars; | |
| } | |
| public class VRCAvatarPedestalAutoplacer : EditorWindow | |
| { | |
| private string jsonTextInput = ""; | |
| private AvatarDataList avatarDataList; | |
| private Vector2 scrollPosition; | |
| private Vector2 jsonScrollPosition; | |
| private bool showPreview = false; | |
| private bool reverseSort = false; // JSONデータをリバースソートするかどうか | |
| // 配置設定 | |
| private Vector3 startPosition = Vector3.zero; | |
| private Vector3 spacing = new Vector3(3f, 0f, 3f); // Z成分を3に修正 | |
| private int pedestalsPerRow = 5; | |
| private int pedestalsPerColumn = 5; // 縦方向のペデスタル数 | |
| private string parentObjectName = "Avatar Pedestal Container"; | |
| private bool useGridLayout = true; | |
| private bool useDualWallLayout = true; // 対面配置オプション | |
| private float wallDistance = 6f; // 壁間の距離 | |
| private float wallThickness = 0.5f; // 壁の厚さ | |
| private bool createWalls = true; // 壁オブジェクトを作成するかどうか | |
| private Color wallColor = Color.gray; // 壁の色 | |
| private bool removePedestalBase = true; // ペデスタルの台座を削除するかどうか | |
| // プレファブ設定 | |
| private GameObject pedestalPrefab; | |
| private bool autoFindPedestalPrefab = true; | |
| [MenuItem("Tools/Avatar Pedestal Auto Placer")] | |
| public static void ShowWindow() | |
| { | |
| VRCAvatarPedestalAutoplacer window = GetWindow<VRCAvatarPedestalAutoplacer>("VRC Avatar Pedestal Auto Placer"); | |
| window.minSize = new Vector2(400, 800); | |
| window.Show(); | |
| } | |
| private void OnEnable() | |
| { | |
| if (autoFindPedestalPrefab) | |
| { | |
| FindPedestalPrefab(); | |
| } | |
| } | |
| private void OnGUI() | |
| { | |
| GUILayout.Label("VRChat Avatar Pedestal Auto Placer", EditorStyles.boldLabel); | |
| GUILayout.Space(10); | |
| // JSON読み込みセクション | |
| EditorGUILayout.LabelField("JSON Data Input", EditorStyles.boldLabel); | |
| // JSONソートオプション | |
| reverseSort = EditorGUILayout.Toggle("Reverse Sort JSON Data", reverseSort); | |
| // テキスト入力モード | |
| EditorGUILayout.LabelField("Paste JSON Data Here:"); | |
| EditorGUILayout.BeginHorizontal(); | |
| if (GUILayout.Button("Clear", GUILayout.Width(60))) | |
| { | |
| jsonTextInput = ""; | |
| GUI.FocusControl(null); | |
| } | |
| if (GUILayout.Button("Paste", GUILayout.Width(60))) | |
| { | |
| jsonTextInput = EditorGUIUtility.systemCopyBuffer; | |
| GUI.FocusControl(null); | |
| } | |
| GUILayout.FlexibleSpace(); | |
| if (GUILayout.Button("Load JSON Text", GUILayout.Width(120))) | |
| { | |
| LoadAvatarDataFromText(); | |
| } | |
| EditorGUILayout.EndHorizontal(); | |
| // スクロール可能なテキストエリア | |
| jsonScrollPosition = EditorGUILayout.BeginScrollView(jsonScrollPosition, GUILayout.Height(150)); | |
| EditorGUILayout.LabelField("JSON Content:", EditorStyles.miniLabel); | |
| jsonTextInput = EditorGUILayout.TextArea(jsonTextInput, GUILayout.ExpandHeight(true)); | |
| EditorGUILayout.EndScrollView(); | |
| // JSONが入力されている場合は自動読み込みボタンを表示 | |
| if (!string.IsNullOrEmpty(jsonTextInput) && jsonTextInput.Trim().Length > 10) | |
| { | |
| GUI.backgroundColor = Color.cyan; | |
| if (GUILayout.Button("Auto Load JSON (Text Changed)", GUILayout.Height(25))) | |
| { | |
| LoadAvatarDataFromText(); | |
| } | |
| GUI.backgroundColor = Color.white; | |
| } | |
| GUILayout.Space(10); | |
| // プレファブ設定セクション | |
| EditorGUILayout.LabelField("Avatar Pedestal Prefab Settings", EditorStyles.boldLabel); | |
| autoFindPedestalPrefab = EditorGUILayout.Toggle("Auto Find Pedestal Prefab", autoFindPedestalPrefab); | |
| if (!autoFindPedestalPrefab) | |
| { | |
| pedestalPrefab = (GameObject)EditorGUILayout.ObjectField("Avatar Pedestal Prefab", pedestalPrefab, typeof(GameObject), false); | |
| } | |
| else | |
| { | |
| EditorGUILayout.BeginHorizontal(); | |
| EditorGUILayout.ObjectField("Found Pedestal Prefab", pedestalPrefab, typeof(GameObject), false); | |
| if (GUILayout.Button("Refresh", GUILayout.Width(80))) | |
| { | |
| FindPedestalPrefab(); | |
| } | |
| EditorGUILayout.EndHorizontal(); | |
| } | |
| GUILayout.Space(10); | |
| // 配置設定セクション | |
| EditorGUILayout.LabelField("Placement Settings", EditorStyles.boldLabel); | |
| parentObjectName = EditorGUILayout.TextField("Parent Object Name", parentObjectName); | |
| EditorGUILayout.HelpBox("All avatar pedestals will be grouped under a single Empty GameObject.", MessageType.Info); | |
| useGridLayout = EditorGUILayout.Toggle("Use Grid Layout", useGridLayout); | |
| if (useGridLayout) | |
| { | |
| pedestalsPerColumn = EditorGUILayout.IntField("Pedestals Per Column", pedestalsPerColumn); | |
| spacing = EditorGUILayout.Vector3Field("Spacing", spacing); | |
| GUILayout.Space(5); | |
| useDualWallLayout = EditorGUILayout.Toggle("Dual Wall Layout", useDualWallLayout); | |
| if (useDualWallLayout) | |
| { | |
| EditorGUILayout.HelpBox("Pedestals will be arranged in columns facing each other through walls.\nLayout: Front] | [Back Front] | [Back", MessageType.Info); | |
| wallDistance = EditorGUILayout.FloatField("Wall Distance", wallDistance); | |
| wallThickness = EditorGUILayout.FloatField("Wall Thickness", wallThickness); | |
| wallColor = EditorGUILayout.ColorField("Wall Color", wallColor); | |
| createWalls = EditorGUILayout.Toggle("Create Wall Objects", createWalls); | |
| } | |
| } | |
| startPosition = EditorGUILayout.Vector3Field("Start Position", startPosition); | |
| GUILayout.Space(10); | |
| // ペデスタル設定セクション | |
| EditorGUILayout.LabelField("Pedestal Settings", EditorStyles.boldLabel); | |
| removePedestalBase = EditorGUILayout.Toggle("Remove Pedestal Base", removePedestalBase); | |
| EditorGUILayout.HelpBox("Automatically removes the base/platform part from avatar pedestals.", MessageType.Info); | |
| GUILayout.Space(10); | |
| // データプレビューセクション | |
| if (avatarDataList != null && avatarDataList.avatars != null) | |
| { | |
| EditorGUILayout.LabelField($"Loaded Avatars: {avatarDataList.avatars.Count}", EditorStyles.boldLabel); | |
| showPreview = EditorGUILayout.Foldout(showPreview, "Avatar Data Preview"); | |
| if (showPreview) | |
| { | |
| scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(200)); | |
| for (int i = 0; i < avatarDataList.avatars.Count; i++) | |
| { | |
| var avatar = avatarDataList.avatars[i]; | |
| EditorGUILayout.LabelField($"{i + 1}. {avatar.name}", EditorStyles.wordWrappedLabel); | |
| EditorGUILayout.LabelField($" ID: {avatar.id}", EditorStyles.miniLabel); | |
| GUILayout.Space(2); | |
| } | |
| EditorGUILayout.EndScrollView(); | |
| } | |
| GUILayout.Space(10); | |
| // 実行ボタン | |
| GUI.backgroundColor = Color.green; | |
| if (GUILayout.Button("Create Avatar Pedestals", GUILayout.Height(40))) | |
| { | |
| CreatePedestals(); | |
| } | |
| GUI.backgroundColor = Color.white; | |
| } | |
| else | |
| { | |
| EditorGUILayout.HelpBox("No avatar data loaded. Please paste JSON data and click 'Load JSON Text'.", MessageType.Info); | |
| } | |
| } | |
| private void FindPedestalPrefab() | |
| { | |
| // VRC_AvatarPedestalプレファブを自動検索 | |
| string[] guids = AssetDatabase.FindAssets("t:Prefab"); | |
| foreach (string guid in guids) | |
| { | |
| string path = AssetDatabase.GUIDToAssetPath(guid); | |
| GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path); | |
| if (prefab != null && prefab.GetComponent<VRCAvatarPedestal>() != null) | |
| { | |
| pedestalPrefab = prefab; | |
| Debug.Log($"Avatar Pedestal prefab found: {path}"); | |
| return; | |
| } | |
| } | |
| // VRC SDK内のプレファブも検索 | |
| string[] vrcGuids = AssetDatabase.FindAssets("AvatarPedestal t:Prefab"); | |
| if (vrcGuids.Length > 0) | |
| { | |
| string path = AssetDatabase.GUIDToAssetPath(vrcGuids[0]); | |
| pedestalPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(path); | |
| Debug.Log($"VRC Avatar Pedestal prefab found: {path}"); | |
| } | |
| else | |
| { | |
| Debug.LogWarning("VRC_AvatarPedestal prefab not found. Please assign manually."); | |
| } | |
| } | |
| private void LoadAvatarDataFromText() | |
| { | |
| if (string.IsNullOrEmpty(jsonTextInput?.Trim())) | |
| { | |
| EditorUtility.DisplayDialog("Error", "JSON text input is empty.", "OK"); | |
| return; | |
| } | |
| try | |
| { | |
| ProcessJsonData(jsonTextInput); | |
| Debug.Log($"Loaded {avatarDataList.avatars.Count} avatars from text input."); | |
| } | |
| catch (System.Exception e) | |
| { | |
| EditorUtility.DisplayDialog("Error", $"Failed to parse JSON text: {e.Message}", "OK"); | |
| Debug.LogError($"JSON Text Parse Error: {e.Message}"); | |
| } | |
| } | |
| private void ProcessJsonData(string jsonContent) | |
| { | |
| // 配列形式のJSONかどうかを判定 | |
| if (jsonContent.TrimStart().StartsWith("[")) | |
| { | |
| // 配列形式の場合はオブジェクトでラップ | |
| jsonContent = "{\"avatars\":" + jsonContent + "}"; | |
| } | |
| else if (!jsonContent.Contains("\"avatars\"")) | |
| { | |
| // オブジェクト形式だが"avatars"キーがない場合の処理 | |
| throw new System.Exception("JSON format not supported. Expected array of {name, id} objects or object with 'avatars' key."); | |
| } | |
| avatarDataList = JsonUtility.FromJson<AvatarDataList>(jsonContent); | |
| if (avatarDataList?.avatars == null || avatarDataList.avatars.Count == 0) | |
| { | |
| throw new System.Exception("No valid avatar data found in JSON."); | |
| } | |
| // リバースソートオプションが有効な場合、リストを逆順にする | |
| if (reverseSort) | |
| { | |
| avatarDataList.avatars.Reverse(); | |
| } | |
| Repaint(); | |
| } | |
| private void CreatePedestals() | |
| { | |
| if (pedestalPrefab == null) | |
| { | |
| EditorUtility.DisplayDialog("Error", "Avatar Pedestal prefab is not assigned.", "OK"); | |
| return; | |
| } | |
| if (avatarDataList?.avatars == null || avatarDataList.avatars.Count == 0) | |
| { | |
| EditorUtility.DisplayDialog("Error", "No avatar data loaded.", "OK"); | |
| return; | |
| } | |
| // 必ず親オブジェクトを作成 | |
| GameObject parentObject = new GameObject(parentObjectName); | |
| parentObject.transform.position = startPosition; | |
| Undo.RegisterCreatedObjectUndo(parentObject, "Create Avatar Pedestal Container"); | |
| if (useDualWallLayout && useGridLayout) | |
| { | |
| CreateDualWallLayout(parentObject); | |
| } | |
| else | |
| { | |
| CreateStandardLayout(parentObject); | |
| } | |
| // 親オブジェクトを選択状態にする | |
| Selection.activeGameObject = parentObject; | |
| } | |
| private void CreateStandardLayout(GameObject parentObject) | |
| { | |
| int pedestalCount = 0; | |
| foreach (var avatar in avatarDataList.avatars) | |
| { | |
| Vector3 position = CalculateStandardPosition(pedestalCount); | |
| GameObject pedestal = (GameObject)PrefabUtility.InstantiatePrefab(pedestalPrefab); | |
| if (pedestal != null) | |
| { | |
| pedestal.transform.SetParent(parentObject.transform); | |
| pedestal.transform.localPosition = position - startPosition; | |
| pedestal.transform.localRotation = Quaternion.identity; | |
| VRCAvatarPedestal avatarPedestal = pedestal.GetComponent<VRCAvatarPedestal>(); | |
| if (avatarPedestal != null) | |
| { | |
| // VRChat SDK3のAvatarPedestalではblueprintIdプロパティを使用 | |
| avatarPedestal.blueprintId = avatar.id; | |
| } | |
| pedestal.name = $"Avatar Pedestal {pedestalCount + 1} - {avatar.name}"; | |
| Undo.RegisterCreatedObjectUndo(pedestal, "Create Avatar Pedestal"); | |
| } | |
| pedestalCount++; | |
| } | |
| EditorUtility.DisplayDialog("Success", $"Created {pedestalCount} avatar pedestals under '{parentObjectName}' successfully!", "OK"); | |
| } | |
| private void CreateDualWallLayout(GameObject parentObject) | |
| { | |
| int totalPedestals = avatarDataList.avatars.Count; | |
| int pedestalsCreated = 0; | |
| int wallsCreated = 0; | |
| // 必要な列数を計算(2列で1組) | |
| int totalGroups = Mathf.CeilToInt((float)totalPedestals / (pedestalsPerColumn * 2)); | |
| for (int group = 0; group < totalGroups; group++) | |
| { | |
| float groupBaseX = group * wallDistance; | |
| // 表側(左側)の列 - 壁の左側に配置 | |
| for (int row = 0; row < pedestalsPerColumn; row++) | |
| { | |
| int pedestalIndex = group * pedestalsPerColumn * 2 + row; | |
| if (pedestalIndex >= totalPedestals) break; | |
| var avatar = avatarDataList.avatars[pedestalIndex]; | |
| Vector3 position = new Vector3( | |
| groupBaseX - wallThickness / 2f - 0.1f, // 壁の左側 | |
| row * spacing.y, | |
| row * spacing.z // Z座標を3ずつずらす | |
| ); | |
| GameObject pedestal = (GameObject)PrefabUtility.InstantiatePrefab(pedestalPrefab); | |
| if (pedestal != null) | |
| { | |
| pedestal.transform.SetParent(parentObject.transform); | |
| pedestal.transform.localPosition = position; | |
| pedestal.transform.localRotation = Quaternion.Euler(0, 90, 0); // 右向き(壁に向かって) | |
| VRCAvatarPedestal avatarPedestal = pedestal.GetComponent<VRCAvatarPedestal>(); | |
| if (avatarPedestal != null) | |
| { | |
| avatarPedestal.blueprintId = avatar.id; | |
| } | |
| pedestal.name = $"Avatar Pedestal {pedestalIndex + 1} - {avatar.name}"; | |
| Undo.RegisterCreatedObjectUndo(pedestal, "Create Avatar Pedestal"); | |
| // ペデスタルの台座を削除 | |
| if (removePedestalBase) | |
| { | |
| RemovePedestalBase(pedestal); | |
| } | |
| pedestalsCreated++; | |
| } | |
| } | |
| // 裏側(右側)の列 - 壁の右側に配置 | |
| for (int row = 0; row < pedestalsPerColumn; row++) | |
| { | |
| int pedestalIndex = group * pedestalsPerColumn * 2 + pedestalsPerColumn + row; | |
| if (pedestalIndex >= totalPedestals) break; | |
| var avatar = avatarDataList.avatars[pedestalIndex]; | |
| Vector3 position = new Vector3( | |
| groupBaseX + wallThickness / 2f + 0.1f, // 壁の右側 | |
| row * spacing.y, | |
| row * spacing.z // Z座標を3ずつずらす | |
| ); | |
| GameObject pedestal = (GameObject)PrefabUtility.InstantiatePrefab(pedestalPrefab); | |
| if (pedestal != null) | |
| { | |
| pedestal.transform.SetParent(parentObject.transform); | |
| pedestal.transform.localPosition = position; | |
| pedestal.transform.localRotation = Quaternion.Euler(0, -90, 0); // 左向き(壁に向かって) | |
| VRCAvatarPedestal avatarPedestal = pedestal.GetComponent<VRCAvatarPedestal>(); | |
| if (avatarPedestal != null) | |
| { | |
| avatarPedestal.blueprintId = avatar.id; | |
| } | |
| pedestal.name = $"Avatar Pedestal {pedestalIndex + 1} - {avatar.name}"; | |
| Undo.RegisterCreatedObjectUndo(pedestal, "Create Avatar Pedestal"); | |
| // ペデスタルの台座を削除 | |
| if (removePedestalBase) | |
| { | |
| RemovePedestalBase(pedestal); | |
| } | |
| pedestalsCreated++; | |
| } | |
| } | |
| // 壁を作成(グループの中央) | |
| if (createWalls) | |
| { | |
| // 壁の高さを計算(元の高さの半分) | |
| float wallHeight = (pedestalsPerColumn * spacing.y + 1f) * 3f; // 元の高さの半分(2倍を削除) | |
| Vector3 wallPosition = new Vector3( | |
| groupBaseX, | |
| wallHeight / 2f, // 壁の中心が壁の高さの半分の位置(底面がY=0) | |
| (pedestalsPerColumn - 1) * spacing.z / 2f // Z座標も中央に | |
| ); | |
| GameObject wall = GameObject.CreatePrimitive(PrimitiveType.Cube); | |
| wall.transform.SetParent(parentObject.transform); | |
| wall.transform.localPosition = wallPosition; | |
| wall.transform.localScale = new Vector3( | |
| wallThickness, | |
| wallHeight, // 調整された高さを使用 | |
| pedestalsPerColumn * spacing.z + 3f // Z方向のサイズも調整 | |
| ); | |
| wall.name = $"Wall {group + 1}"; | |
| Renderer renderer = wall.GetComponent<Renderer>(); | |
| if (renderer != null) | |
| { | |
| renderer.material.color = wallColor; | |
| } | |
| Undo.RegisterCreatedObjectUndo(wall, "Create Wall"); | |
| wallsCreated++; | |
| } | |
| } | |
| EditorUtility.DisplayDialog("Success", | |
| $"Created {pedestalsCreated} avatar pedestals and {wallsCreated} walls under '{parentObjectName}' successfully!", | |
| "OK"); | |
| } | |
| private Vector3 CalculateStandardPosition(int index) | |
| { | |
| if (useGridLayout) | |
| { | |
| int col = index / pedestalsPerColumn; | |
| int row = index % pedestalsPerColumn; | |
| return startPosition + new Vector3( | |
| col * spacing.x, | |
| row * spacing.y, | |
| col * spacing.z | |
| ); | |
| } | |
| else | |
| { | |
| return startPosition + (spacing * index); | |
| } | |
| } | |
| private void RemovePedestalBase(GameObject pedestal) | |
| { | |
| // ペデスタルにあるMesh Rendererを全て取得(通常は1つのみ) | |
| MeshRenderer[] allRenderers = pedestal.GetComponentsInChildren<MeshRenderer>(true); | |
| Debug.Log($"Found {allRenderers.Length} MeshRenderer(s) in {pedestal.name}"); | |
| foreach (MeshRenderer renderer in allRenderers) | |
| { | |
| Debug.Log($"Disabling MeshRenderer on: {renderer.gameObject.name}"); | |
| Undo.RecordObject(renderer, "Disable Pedestal Base Renderer"); | |
| renderer.enabled = false; | |
| } | |
| // 確認のため、無効化後の状態をログ出力 | |
| Debug.Log($"Pedestal base removal completed for {pedestal.name}"); | |
| } | |
| private int GetDepth(Transform transform, Transform root) | |
| { | |
| int depth = 0; | |
| Transform current = transform; | |
| while (current != root && current != null) | |
| { | |
| depth++; | |
| current = current.parent; | |
| } | |
| return depth; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // VRChat Favorite Avatars ID Extractor | |
| // Chrome DevTools Snippetで実行してください | |
| (function() { | |
| console.log('VRChat Favorite Avatars ID Extractor - 開始'); | |
| // 詳細なDOM解析を行う関数 | |
| function deepScanForAvatarIds() { | |
| console.log('詳細スキャンを開始...'); | |
| const avatarIds = []; | |
| // 全てのテキストノードとdata属性をスキャン | |
| const walker = document.createTreeWalker( | |
| document.body, | |
| NodeFilter.SHOW_ELEMENT, | |
| null, | |
| false | |
| ); | |
| let node; | |
| while (node = walker.nextNode()) { | |
| // data属性をチェック | |
| for (const attr of node.attributes || []) { | |
| if (attr.value.includes('avtr_')) { | |
| const matches = attr.value.match(/avtr_[a-zA-Z0-9_-]+/g); | |
| if (matches) { | |
| matches.forEach(match => { | |
| if (!avatarIds.includes(match)) { | |
| avatarIds.push(match); | |
| console.log(`詳細スキャンでAvatar ID発見: ${match} (属性: ${attr.name})`); | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| // テキストコンテンツをチェック | |
| const text = node.textContent || ''; | |
| const matches = text.match(/avtr_[a-zA-Z0-9_-]+/g); | |
| if (matches) { | |
| matches.forEach(match => { | |
| if (!avatarIds.includes(match)) { | |
| avatarIds.push(match); | |
| console.log(`詳細スキャンでAvatar ID発見: ${match} (テキスト)`); | |
| } | |
| }); | |
| } | |
| } | |
| return avatarIds; | |
| } | |
| // ReactのFiberノードから情報を抽出する関数 | |
| function scanReactFiber() { | |
| console.log('React Fiberスキャンを開始...'); | |
| const avatarIds = []; | |
| // React Fiberルートを探す | |
| const reactRoots = document.querySelectorAll('[data-reactroot], #root, .react-root'); | |
| reactRoots.forEach(root => { | |
| const fiberKey = Object.keys(root).find(key => key.startsWith('__reactInternalInstance') || key.startsWith('_reactInternalFiber')); | |
| if (fiberKey) { | |
| console.log('React Fiberが見つかりました'); | |
| // Fiberツリーを再帰的に探索(深すぎないように制限) | |
| function traverseFiber(fiber, depth = 0) { | |
| if (!fiber || depth > 10) return; | |
| if (fiber.memoizedProps) { | |
| const props = JSON.stringify(fiber.memoizedProps); | |
| const matches = props.match(/avtr_[a-zA-Z0-9_-]+/g); | |
| if (matches) { | |
| matches.forEach(match => { | |
| if (!avatarIds.includes(match)) { | |
| avatarIds.push(match); | |
| console.log(`React FiberでAvatar ID発見: ${match}`); | |
| } | |
| }); | |
| } | |
| } | |
| // 子要素を探索 | |
| if (fiber.child) traverseFiber(fiber.child, depth + 1); | |
| if (fiber.sibling) traverseFiber(fiber.sibling, depth + 1); | |
| } | |
| traverseFiber(root[fiberKey]); | |
| } | |
| }); | |
| return avatarIds; | |
| } | |
| // ネットワークリクエストを監視する関数 | |
| function monitorNetworkRequests() { | |
| console.log('ネットワーク監視を開始(5秒間)...'); | |
| const originalFetch = window.fetch; | |
| const foundIds = []; | |
| window.fetch = function(...args) { | |
| const url = args[0]; | |
| return originalFetch.apply(this, args).then(response => { | |
| if (url.includes('avatar') || url.includes('favorite')) { | |
| response.clone().text().then(text => { | |
| const matches = text.match(/avtr_[a-zA-Z0-9_-]+/g); | |
| if (matches) { | |
| matches.forEach(match => { | |
| if (!foundIds.includes(match)) { | |
| foundIds.push(match); | |
| console.log(`ネットワークリクエストでAvatar ID発見: ${match}`); | |
| } | |
| }); | |
| } | |
| }).catch(() => {}); | |
| } | |
| return response; | |
| }); | |
| }; | |
| // 5秒後に元のfetch関数を復元 | |
| setTimeout(() => { | |
| window.fetch = originalFetch; | |
| console.log('ネットワーク監視終了'); | |
| }, 5000); | |
| return foundIds; | |
| } | |
| // メイン実行 | |
| console.log('=== Avatar IDとアバター名を収集中 ==='); | |
| // アバター名を取得する関数 | |
| function getAvatarName(avatarId, element) { | |
| let avatarName = 'Unknown Avatar'; | |
| if (element) { | |
| // まず、要素の周辺でh4要素を探す | |
| const h4Element = element.querySelector('h4') || | |
| element.closest('.avatar-item, .favorite-avatar, [class*="avatar"]')?.querySelector('h4') || | |
| element.parentElement?.querySelector('h4'); | |
| if (h4Element) { | |
| // h4のtitle属性を最優先 | |
| if (h4Element.title && h4Element.title.trim()) { | |
| avatarName = h4Element.title.trim(); | |
| } | |
| // h4のテキストコンテンツ | |
| else if (h4Element.textContent && h4Element.textContent.trim()) { | |
| avatarName = h4Element.textContent.trim(); | |
| } | |
| } | |
| // h4が見つからない場合は他の方法を試す | |
| if (avatarName === 'Unknown Avatar') { | |
| // 要素自体のtitle属性 | |
| if (element.title && element.title.trim() && !element.title.includes('Avatar Image')) { | |
| avatarName = element.title.trim(); | |
| } | |
| // aria-label属性 | |
| else if (element.getAttribute('aria-label') && !element.getAttribute('aria-label').includes('Avatar Image')) { | |
| avatarName = element.getAttribute('aria-label'); | |
| } | |
| // 近くのテキスト要素を探す | |
| else { | |
| const textElements = element.querySelectorAll('h1, h2, h3, h4, h5, h6, .title, [class*="title"], [class*="name"]'); | |
| for (const textEl of textElements) { | |
| const text = textEl.textContent?.trim(); | |
| if (text && text !== avatarId && !text.includes('avtr_') && !text.includes('Avatar Image')) { | |
| avatarName = text; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // まだ見つからない場合は親要素を遡って探す | |
| if (avatarName === 'Unknown Avatar') { | |
| let parent = element.parentElement; | |
| let attempts = 0; | |
| while (parent && attempts < 3) { | |
| // 親要素内でh4を探す | |
| const h4InParent = parent.querySelector('h4'); | |
| if (h4InParent && h4InParent.title) { | |
| avatarName = h4InParent.title.trim(); | |
| break; | |
| } else if (h4InParent && h4InParent.textContent) { | |
| const text = h4InParent.textContent.trim(); | |
| if (text && !text.includes('Avatar Image')) { | |
| avatarName = text; | |
| break; | |
| } | |
| } | |
| parent = parent.parentElement; | |
| attempts++; | |
| } | |
| } | |
| } | |
| // Avatar IDが含まれていたら除去 | |
| avatarName = avatarName.replace(/avtr_[a-zA-Z0-9_-]+/g, '').trim(); | |
| // 「Avatar Image」は除外 | |
| if (avatarName.includes('Avatar Image') || avatarName === 'Avatar Image') { | |
| avatarName = 'Unknown Avatar'; | |
| } | |
| // 長すぎる場合は短縮 | |
| if (avatarName.length > 100) { | |
| avatarName = avatarName.substring(0, 97) + '...'; | |
| } | |
| return avatarName || 'Unknown Avatar'; | |
| } | |
| // Avatar IDと名前のペアを収集する関数 | |
| function collectAvatarData() { | |
| const avatarData = []; | |
| const processedIds = new Set(); | |
| // 各セレクターで要素とAvatar IDの関連を保持 | |
| const selectors = [ | |
| 'a[href*="/home/avatar/"]', | |
| 'a[href*="/avatar/"]', | |
| '[data-avatar-id]', | |
| '.avatar-item', | |
| '.favorite-avatar', | |
| 'div[id*="avatar"]', | |
| 'div[class*="avatar"]', | |
| 'h4[title]', // h4要素も直接検索 | |
| '[class*="css-"] h4', // CSSクラスを持つh4要素 | |
| ]; | |
| selectors.forEach(selector => { | |
| try { | |
| const elements = document.querySelectorAll(selector); | |
| elements.forEach(element => { | |
| let avatarId = null; | |
| // h4要素の場合は周辺でAvatar IDを探す | |
| if (element.tagName === 'H4') { | |
| // h4要素の親や兄弟要素でAvatar IDを探す | |
| let searchElement = element.parentElement; | |
| let attempts = 0; | |
| while (searchElement && attempts < 3) { | |
| // href属性を持つリンクを探す | |
| const linkElement = searchElement.querySelector('a[href*="/avatar/"]'); | |
| if (linkElement && linkElement.href) { | |
| const match = linkElement.href.match(/\/avatar\/([a-zA-Z0-9_-]+)/); | |
| if (match) { | |
| avatarId = match[1]; | |
| break; | |
| } | |
| } | |
| // data属性を探す | |
| const dataElement = searchElement.querySelector('[data-avatar-id]'); | |
| if (dataElement) { | |
| avatarId = dataElement.dataset.avatarId; | |
| break; | |
| } | |
| // テキストコンテンツでAvatar IDを探す | |
| const text = searchElement.textContent || ''; | |
| const match = text.match(/avtr_[a-zA-Z0-9_-]+/); | |
| if (match) { | |
| avatarId = match[0]; | |
| break; | |
| } | |
| searchElement = searchElement.parentElement; | |
| attempts++; | |
| } | |
| } | |
| // 通常の要素の場合 | |
| else { | |
| // href属性からavatar IDを抽出 | |
| if (element.href) { | |
| const match = element.href.match(/\/avatar\/([a-zA-Z0-9_-]+)/); | |
| if (match) { | |
| avatarId = match[1]; | |
| } | |
| } | |
| // data属性からavatar IDを抽出 | |
| if (!avatarId && element.dataset) { | |
| avatarId = element.dataset.avatarId || | |
| element.dataset.id || | |
| element.dataset.avatarid; | |
| } | |
| // テキストコンテンツからavatar IDを抽出 | |
| if (!avatarId) { | |
| const text = element.textContent || element.innerText || ''; | |
| const match = text.match(/avtr_[a-zA-Z0-9_-]+/); | |
| if (match) { | |
| avatarId = match[0]; | |
| } | |
| } | |
| } | |
| if (avatarId && !processedIds.has(avatarId)) { | |
| processedIds.add(avatarId); | |
| const avatarName = getAvatarName(avatarId, element); | |
| avatarData.push({ name: avatarName, id: avatarId }); | |
| console.log(`Avatar発見: ${avatarName} (${avatarId})`); | |
| } | |
| }); | |
| } catch (error) { | |
| console.warn(`セレクター "${selector}" でエラー:`, error); | |
| } | |
| }); | |
| // 詳細スキャンで見つかったIDの名前も探す | |
| const deepScanIds = deepScanForAvatarIds(); | |
| deepScanIds.forEach(avatarId => { | |
| if (!processedIds.has(avatarId)) { | |
| processedIds.add(avatarId); | |
| // IDのみの場合は要素を再検索 | |
| const elements = document.querySelectorAll(`[href*="${avatarId}"], [data-avatar-id="${avatarId}"]`); | |
| const avatarName = elements.length > 0 ? getAvatarName(avatarId, elements[0]) : 'Unknown Avatar'; | |
| avatarData.push({ name: avatarName, id: avatarId }); | |
| } | |
| }); | |
| return avatarData; | |
| } | |
| // 結果出力 | |
| setTimeout(() => { | |
| console.log('\n=== Avatar IDと名前を収集中 ==='); | |
| const avatarData = collectAvatarData(); | |
| console.log('\n=== 結果 ==='); | |
| console.log(`発見されたアバター数: ${avatarData.length}`); | |
| console.log('\nアバターリスト(アバター名: avatarid):'); | |
| if (avatarData.length > 0) { | |
| avatarData.forEach((avatar, index) => { | |
| console.log(`${avatar.name}: ${avatar.id}`); | |
| }); | |
| console.log('\n=== コピー用フォーマット ==='); | |
| const formattedList = avatarData.map(avatar => `${avatar.name}: ${avatar.id}`).join('\n'); | |
| console.log(formattedList); | |
| // JSON形式でも出力 | |
| console.log('\n=== JSON形式 ==='); | |
| console.log(JSON.stringify(avatarData, null, 2)); | |
| } else { | |
| console.log('Avatar IDが見つかりませんでした。'); | |
| console.log('以下を確認してください:'); | |
| console.log('1. VRChatのFavorite Avatarsページにいるか'); | |
| console.log('2. ページが完全に読み込まれているか'); | |
| console.log('3. ログインしているか'); | |
| console.log('\nページの構造を確認するため、以下を実行してみてください:'); | |
| console.log('document.querySelectorAll("*").length'); // 総要素数 | |
| console.log('document.body.innerHTML.includes("avatar")'); // avatarという文字列があるか | |
| } | |
| }, 6000); // ネットワーク監視終了後に結果表示 | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment