您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Create mod presets to use with Reforger server configs.
// ==UserScript== // @name ArmA Reforger Workshop - Preset Maker // @namespace reforger_preset_maker // @version 1.2 // @description Create mod presets to use with Reforger server configs. // @author JJayRex // @match https://reforger.armaplatform.com/workshop* // @grant none // @run-at document-idle // @license GPL-3.0-or-later // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'reforgerPreset'; // Storage helpers function loadMods() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; } } function saveMods(mods) { localStorage.setItem(STORAGE_KEY, JSON.stringify(mods)); refreshPanel(); } function upsertMod(mod) { const mods = loadMods(); const idx = mods.findIndex(m => m.modId === mod.modId); if (idx >= 0) mods[idx] = mod; else mods.push(mod); saveMods(mods); } function removeMod(modId) { const mods = loadMods().filter(m => m.modId !== modId); saveMods(mods); } // UI function ensureUI() { if (document.getElementById('presetToggle')) return; const btn = document.createElement('button'); btn.id = 'presetToggle'; btn.textContent = 'Preset'; Object.assign(btn.style, { position: 'fixed', bottom: '10px', right: '20px', zIndex: '999999', background: '#0d6efd', color: '#fff', border: 'none', padding: '6px 10px', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', boxShadow: '0 2px 8px rgba(0,0,0,.3)' }); document.body.appendChild(btn); const panel = document.createElement('div'); panel.id = 'presetPanel'; Object.assign(panel.style, { display: 'none', position: 'fixed', bottom: '60px', right: '20px', zIndex: '999999', background: 'rgba(20,20,20,.95)', color: '#eee', padding: '12px', border: '1px solid #444', borderRadius: '8px', maxHeight: '75vh', overflowY: 'auto', width: '460px', boxShadow: '0 8px 24px rgba(0,0,0,.4)', backdropFilter: 'blur(2px)' }); const header = document.createElement('div'); header.style.display = 'flex'; header.style.gap = '8px'; header.style.alignItems = 'center'; header.style.marginBottom = '8px'; const title = document.createElement('strong'); title.textContent = 'Mod Preset'; title.style.flex = '1'; const clearBtn = document.createElement('button'); clearBtn.textContent = 'Clear all'; Object.assign(clearBtn.style, smallBtnStyle('#6c757d')); const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close'; Object.assign(closeBtn.style, smallBtnStyle('#dc3545')); header.appendChild(title); header.appendChild(clearBtn); header.appendChild(closeBtn); const listWrap = document.createElement('div'); listWrap.id = 'modListWrap'; listWrap.style.marginBottom = '10px'; listWrap.style.border = '1px solid #333'; listWrap.style.borderRadius = '6px'; listWrap.style.padding = '8px'; listWrap.style.background = '#1d1f20'; const jsonTitle = h3('JSON'); const jsonArea = ta('modJsonView', 140); const strTitle = h3('STRING'); const strArea = ta('modStringView', 60); strArea.setAttribute('wrap', 'off'); strArea.style.overflowX = 'auto'; panel.append(header, listWrap, jsonTitle, jsonArea, strTitle, strArea); document.body.appendChild(panel); btn.addEventListener('click', () => { panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; if (panel.style.display === 'block') refreshPanel(); }); closeBtn.addEventListener('click', () => panel.style.display = 'none'); clearBtn.addEventListener('click', () => { if (confirm('Clear entire mod preset?')) saveMods([]); }); } function smallBtnStyle(bg) { return { background: bg, color: '#fff', border: 'none', padding: '4px 8px', borderRadius: '6px', cursor: 'pointer', fontSize: '12px' }; } function h3(text) { const el = document.createElement('h3'); el.textContent = text; el.style.margin = '8px 0 4px 0'; el.style.fontSize = '14px'; return el; } function ta(id, heightPx) { const el = document.createElement('textarea'); el.id = id; el.readOnly = true; Object.assign(el.style, { width: '100%', height: `${heightPx}px`, fontFamily: 'monospace', fontSize: '12px', background: '#111', color: '#eee', border: '1px solid #333', borderRadius: '6px', padding: '6px' }); return el; } function refreshPanel() { const panel = document.getElementById('presetPanel'); if (!panel) return; const mods = loadMods(); // List const listWrap = document.getElementById('modListWrap'); listWrap.innerHTML = ''; if (mods.length === 0) { const empty = document.createElement('div'); empty.textContent = 'No mods added yet.'; empty.style.opacity = '0.8'; listWrap.appendChild(empty); } else { for (const m of mods) { const row = document.createElement('div'); row.style.display = 'grid'; row.style.gridTemplateColumns = '1fr auto'; row.style.alignItems = 'center'; row.style.gap = '6px'; row.style.padding = '4px 0'; const info = document.createElement('div'); info.innerHTML = `<div><b>${escapeHtml(m.name || '')}</b></div> <div style="opacity:.8">ID: ${escapeHtml(m.modId)}</div>`; const rm = document.createElement('button'); rm.textContent = 'Remove'; Object.assign(rm.style, smallBtnStyle('#dc3545')); rm.addEventListener('click', () => removeMod(m.modId)); row.append(info, rm); listWrap.appendChild(row); const hr = document.createElement('hr'); hr.style.border = 'none'; hr.style.borderTop = '1px solid #2b2b2b'; listWrap.appendChild(hr); } if (listWrap.lastChild && listWrap.lastChild.tagName === 'HR') listWrap.removeChild(listWrap.lastChild); } // JSON view const jsonArea = document.getElementById('modJsonView'); jsonArea.value = JSON.stringify(mods, null, 2); // STRING view const ids = mods.map(m => m.modId).filter(Boolean); document.getElementById('modStringView').value = `-addon ${ids.join(', ')}`; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m])); } // Add button function isModPage() { return /\/workshop\/[0-9A-Fa-f]{16}(?:-|$)/.test(location.pathname); } function getIdFromUrl() { const m = location.pathname.match(/\/workshop\/([0-9A-Fa-f]{16})/); return m ? m[1].toUpperCase() : ''; } async function ensureAddButton() { if (!isModPage()) return; // Avoid duplicates if (document.getElementById('addToPresetBtn')) return; // Wait for H1 const h1 = await waitForElm('main h1, main section h1', 8000).catch(() => null); // Create button const addBtn = document.createElement('button'); addBtn.id = 'addToPresetBtn'; addBtn.textContent = 'Add to Preset'; Object.assign(addBtn.style, smallBtnStyle('#28a745')); addBtn.style.fontSize = '13px'; addBtn.style.margin = '6px 0'; // Place near top const anchor = h1 || document.querySelector('main section, main'); if (h1 && h1.parentNode) { h1.parentNode.insertBefore(addBtn, h1.nextSibling); } else if (anchor) { anchor.insertBefore(addBtn, anchor.firstChild); } else { document.body.insertBefore(addBtn, document.body.firstChild); } addBtn.addEventListener('click', async () => { addBtn.disabled = true; const orig = addBtn.textContent; try { const data = await scrapeModData(); if (!data.modId) throw new Error('Mod ID not found'); upsertMod(data); const deps = collectDependencies(); let msg = 'Added!'; if (deps.length > 0) { deps.forEach(d => upsertMod(d)); msg = `Added (+${deps.length} deps)`; } addBtn.textContent = 'Added!'; setTimeout(() => addBtn.textContent = orig, 1500); } catch (e) { console.warn(e); addBtn.textContent = 'Failed (see console)'; setTimeout(() => addBtn.textContent = orig, 2000); } finally { addBtn.disabled = false; } }); } async function scrapeModData() { const h1 = document.querySelector('main h1, main section h1'); const name = (h1?.textContent || '').trim(); let modId = ''; // Try semantic pass document.querySelectorAll('dl, div').forEach(dl => { // Only inspect blocks that look like info rows const rows = dl.querySelectorAll(':scope > div'); rows.forEach(div => { const dt = div.querySelector('dt'); const dd = div.querySelector('dd'); if (!dt || !dd) return; const label = dt.textContent.trim().toLowerCase(); if (label === 'id') { const span = dd.querySelector('span'); modId = (span ? span.textContent : dd.textContent).trim(); } }); }); // Fallbacks if (!modId) modId = getIdFromUrl(); // Normalize modId = (modId || '').toUpperCase(); return { modId, name }; } // Dependencies function xpevalFirst(xpath) { try { return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } catch { return null; } } // Try the user-given container first function findDependenciesContainer() { const exact = xpevalFirst('/html/body/div[1]/div/main/div/section/div[2]/div[2]/section/div'); return exact; } function collectDependencies() { const container = findDependenciesContainer(); if (!container) return []; const anchors = Array.from(container.querySelectorAll('a[href^="/workshop/"][href*="-"]')); const currentId = getIdFromUrl(); const results = []; for (const a of anchors) { const href = a.getAttribute('href') || ''; const m = href.match(/\/workshop\/([0-9A-Fa-f]{16})-([^/?#]+)/); if (!m) continue; const modId = m[1].toUpperCase(); if (modId === currentId) continue; // skip self const slug = decodeURIComponent(m[2]); const name = deriveNameFromAnchor(a, slug); results.push({ modId, name }); } return uniqBy(results, x => x.modId); } function deriveNameFromAnchor(a, slug) { // Prefer visible text on the dependency card/link let txt = (a.innerText || a.textContent || '').trim(); if (txt) { // Take first non-empty line (cards often have multiple lines) const firstLine = txt.split('\n').map(s => s.trim()).filter(Boolean)[0]; if (firstLine) return firstLine; } // Fallback: guess from slug: underscores -> spaces, keep hyphens (e.g., UH-60) let s = (slug || '').replace(/_/g, ' '); // Insert spaces for CamelCase boundaries: ProjectRedline -> Project Redline s = s.replace(/([a-z])([A-Z])/g, '$1 $2'); return s.trim(); } function uniqBy(arr, keyFn) { const seen = new Set(); const out = []; for (const it of arr) { const k = keyFn(it); if (!seen.has(k)) { seen.add(k); out.push(it); } } return out; } // Utils function waitForElm(selector, timeoutMs = 5000) { return new Promise((resolve, reject) => { const found = document.querySelector(selector); if (found) return resolve(found); const obs = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { obs.disconnect(); resolve(el); } }); obs.observe(document.documentElement, { childList: true, subtree: true }); if (timeoutMs > 0) { setTimeout(() => { obs.disconnect(); reject(new Error(`Timeout waiting for ${selector}`)); }, timeoutMs); } }); } // Hook SPA navigation function hookRouting() { const fire = () => window.dispatchEvent(new Event('locationchange')); const _ps = history.pushState; history.pushState = function () { const r = _ps.apply(this, arguments); fire(); return r; }; const _rs = history.replaceState; history.replaceState = function () { const r = _rs.apply(this, arguments); fire(); return r; }; window.addEventListener('popstate', fire); window.addEventListener('locationchange', () => { // Attempt to add button on route change setTimeout(ensureAddButton, 0); }); } // Boot function boot() { ensureUI(); hookRouting(); ensureAddButton(); // Just in case const mo = new MutationObserver(() => { if (isModPage() && !document.getElementById('addToPresetBtn')) { clearTimeout(mo._t); mo._t = setTimeout(ensureAddButton, 150); } }); mo.observe(document.body, { childList: true, subtree: true }); } // Delay boot if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })();