언리얼 엔진 공식 홈페이지 : 가장 강력한 리얼타임 3D 창작 툴 - Unreal Engine
언리얼 엔진에서 잘 사용했던 퀵셀 메가스캔 에셋들이 내년부터 유료화된다.
대신 콘텐츠 마켓플레이스 팹(fab)이 생긴다고 한다.
따라서 무료로 사용하려면 올해 안에 모든 에셋을 구매해야하는데, 수만가지의 에셋을 마우스로 클릭해서 받으려면 오래 걸린다.
github에서 퀵셀 메가스캔 아이템들을 자동으로 내 계정에 구매해주는 스크립트를 공유한다.
<크롬 OS 기준> 맥북으로 작성했다.
1. https://quixel.com/megascans/home/ 에서 우측 상단 로그인 버튼을 눌러서 에픽게임즈 계정으로 로그인한다.
2. 해당 창에서 개발자 도구 -> 콘솔로 진입한다.
3. allow pasting을 입력하여 복사붙여넣기를 허용해준다.
4. 아래 코드를 붙여넣는다.
(async (startPage = 0, autoClearConsole = true) => {
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const fetchWithTimeout = (resource, options = {}) => {
const { timeout = 10000 } = options;
return Promise.race([
fetch(resource, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), timeout)
),
]);
};
const callCacheApi = async (params = {}) => {
const defaultParams = {
page: 0,
maxValuesPerFacet: 1000,
hitsPerPage: 1000,
attributesToRetrieve: ["id", "name"].join(","),
};
const fetchData = async () => {
const response = await fetchWithTimeout("https://proxy-algolia-prod.quixel.com/algolia/cache", {
headers: {
"x-api-key": "2Zg8!d2WAHIUW?pCO28cVjfOt9seOWPx@2j",
},
body: JSON.stringify({
url: "https://6UJ1I5A072-2.algolianet.com/1/indexes/assets/query?x-algolia-application-id=6UJ1I5A072&x-algolia-api-key=e93907f4f65fb1d9f813957bdc344892",
params: new URLSearchParams({ ...defaultParams, ...params }).toString(),
}),
method: "POST",
});
if (!response.ok) {
throw new Error(`Error fetching from Cache API: ${response.statusText}`);
}
return await response.json();
};
return await retryOperation(fetchData, 2000, 5);
};
const callAcl = async ({ id, name }) => {
const fetchData = async () => {
const response = await fetchWithTimeout("https://quixel.com/v1/acl", {
headers: {
authorization: "Bearer " + authToken,
"content-type": "application/json;charset=UTF-8",
},
body: JSON.stringify({ assetID: id }),
method: "POST",
});
if (!response.ok) {
throw new Error(`Error adding item ${id} | ${name}: ${response.statusText}`);
}
const json = await response.json();
if (json?.isError) {
console.error(` --> **Failed to add item** Item ${id} | ${name} (${json?.msg})`);
} else {
console.log(` --> Added item ${id} | ${name}`);
}
};
return await retryOperation(fetchData, 2000, 5);
};
const callAcquired = async () => {
const fetchData = async () => {
const response = await fetchWithTimeout("https://quixel.com/v1/assets/acquired", {
headers: {
authorization: "Bearer " + authToken,
"content-type": "application/json;charset=UTF-8",
},
method: "GET",
});
if (!response.ok) {
throw new Error(`Error fetching acquired items: ${response.statusText}`);
}
return await response.json();
};
return await retryOperation(fetchData, 2000, 5);
};
const retryOperation = async (operation, delay, retries) => {
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
console.warn(`Attempt ${attempt} failed (${error.message}). Retrying in ${delay}ms...`);
await sleep(delay);
delay *= 2; // Exponential backoff
}
}
throw lastError;
};
let authToken = "";
const initialize = async () => {
console.log("-> Checking Auth API Token...");
try {
const authCookie = getCookie("auth") ?? "{}";
authToken = JSON.parse(decodeURIComponent(authCookie))?.token;
if (!authToken) {
throw new Error("-> Error: Authentication token not found. Please log in again.");
}
} catch (_) {
throw new Error("-> Error: Authentication token not found. Please log in again.");
}
console.log("-> Fetching acquired items...");
acquiredItems = (await callAcquired()).map((a) => a.assetID);
console.log("-> Fetching total number of pages...");
const initialData = await callCacheApi();
totalPages = initialData.nbPages;
itemsPerPage = initialData.hitsPerPage;
totalItems = initialData.nbHits;
console.log("-> ==============================================");
console.log(`-> Total items: ${totalItems}`);
console.log(`-> ${totalPages} total pages with ${itemsPerPage} items per page`);
console.log(`-> Total items to add: ${totalItems - acquiredItems.length}.`);
console.log("-> ==============================================");
if (!confirm(`Click OK to add ${totalItems - acquiredItems.length} items to your account.`)) {
throw new Error("-> Process cancelled by user.");
}
};
let acquiredItems = [];
let totalPages = 0;
let itemsPerPage = 0;
let totalItems = 0;
const MAX_CONCURRENT_REQUESTS = 5;
const mainProcess = async () => {
for (let pageIdx = startPage || 0; pageIdx < totalPages; pageIdx++) {
console.log(`-> ======================= PAGE ${pageIdx + 1}/${totalPages} START =======================`);
console.log("-> Fetching items from page " + (pageIdx + 1) + " ...");
const pageData = await callCacheApi({ page: pageIdx });
const items = pageData.hits;
console.log("-> Adding unacquired items...");
// Filter out already acquired items
const unownedItems = items.filter((i) => !acquiredItems.includes(i.id));
// Save current progress in localStorage
localStorage.setItem('currentPage', pageIdx);
// Limit concurrent requests
const queue = [...unownedItems];
const workers = Array.from({ length: MAX_CONCURRENT_REQUESTS }, async () => {
while (queue.length > 0) {
const item = queue.shift();
try {
await callAcl(item);
} catch (error) {
console.error(`Error with item ${item.id}: ${error.message}`);
}
}
});
await Promise.all(workers);
console.log(`-> ======================= PAGE ${pageIdx + 1}/${totalPages} COMPLETED =======================`);
if (autoClearConsole) console.clear();
}
};
const finalize = async () => {
console.log("-> Fetching new acquisition info...");
const newAcquiredItems = await callAcquired();
const newItemsAcquired = newAcquiredItems.length;
const newTotalCount = (await callCacheApi()).nbHits;
console.log(`-> Completed. Your account now has a total of ${newItemsAcquired} out of ${newTotalCount} items.`);
alert(`-> Your account now has a total of ${newItemsAcquired} out of ${newTotalCount} items.\n\nIf you find some items missing, try refreshing the page and run the script again.`);
};
try {
// Check if progress was saved
const savedPage = localStorage.getItem('currentPage');
if (savedPage !== null) {
startPage = parseInt(savedPage, 10);
console.log(`-> Resuming from page ${startPage + 1}`);
}
await initialize();
await mainProcess();
await finalize();
// Clear progress
localStorage.removeItem('currentPage');
} catch (error) {
console.error(error.message);
console.log("-> The script could not be completed.");
}
})();
위의 코드는 출처 (A script to automatically add ALL items to your account in quixel · GitHub) 에서 elvincuandra 라는 사용자가 9시간 전에 작성한 코드임을 밝힌다. 기타 다른 버그는 위의 링크를 참조해보면 좋을 것 같다.
반응형