NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name OGame Espionnage Tracker (multi-univers/joueurs, menu fiable)
// @namespace http://tampermonkey.net/
// @version 23.1
// @description Menu espionnage avec stockage séparé par univers et joueur, filtres dépendants, export/import CSV, graphe horaire (global ou joueur) basé sur les résultats filtrés, parsing OGLight + regex, tri par date/heure.
// @author Carim
// @license MIT
// @match https://*.ogame.gameforge.com/game/index.php?page=ingame&component=messages*
// @grant GM_addStyle
// ==/UserScript==
(function() {
'use strict';
// =========================
// Clés de stockage par univers et joueur
// =========================
function getUniverseKey() {
const host = window.location.host; // ex: s123-fr.ogame.gameforge.com
return host.split(".")[0]; // ex: "s123-fr"
}
function getPlayerName() {
// Ajuste le sélecteur si besoin (selon thème/extension)
const el = document.querySelector("#playerName, .playerName, #bar .playername");
return el ? el.textContent.trim() : "UnknownPlayer";
}
function getStorageKey() {
return "spyHistory_" + getUniverseKey() + "_" + getPlayerName();
}
// État
let history = JSON.parse(localStorage.getItem(getStorageKey()) || "[]");
function saveHistory() {
localStorage.setItem(getStorageKey(), JSON.stringify(history));
}
let spyChart = null;
// =========================
// Librairie graphe
// =========================
const chartScript = document.createElement("script");
chartScript.src = "https://cdn.jsdelivr.net/npm/chart.js";
chartScript.async = true;
document.head.appendChild(chartScript);
// =========================
// Styles
// =========================
GM_addStyle(`
#spyMenu {position:fixed;top:80px;right:20px;width:300px;background:#111;color:#eee;
border:1px solid #666;font-size:12px;z-index:999999;transition:width 0.25s;}
#spyMenuHeader {display:flex;align-items:center;justify-content:space-between;
padding:8px;border-bottom:1px solid #666;background:#101010;}
#spyMenuHeaderTitle {font-weight:bold;}
#toggleSpyMenu {background:#333;border:1px solid #666;color:#eee;font-size:14px;
cursor:pointer;padding:4px 8px;border-radius:3px;}
#spyMenuBody {padding:10px;}
#spyMenu.collapsed {width:56px;}
#spyMenu.collapsed #spyMenuHeaderTitle {display:none;}
#spyMenu.collapsed #spyMenuBody {display:none;}
.spy-btn {display:block;width:100%;margin:6px 0;padding:8px;background:#333;color:#eee;
border:1px solid #666;cursor:pointer;text-align:center;}
.spy-btn:hover {background:#3a3a3a;}
#spyTop{margin-top:10px;border-top:1px solid #666;padding-top:10px;}
#spyTop h4 {margin:0 0 6px 0;}
#spyTop ul {list-style:none;padding:0;margin:0;}
#spyTop li {margin:3px 0;}
#spyNotification {position:fixed;bottom:20px;right:20px;background:#222;color:#eee;
padding:10px;border:1px solid #666;border-radius:5px;z-index:1000001;opacity:0;transition:opacity 0.4s;}
#spyHistoryPopup {position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);
display:none;align-items:center;justify-content:center;z-index:1000000;}
#spyHistoryContent {background:#111;color:#eee;padding:20px;border:1px solid #666;width:840px;
max-height:80%;overflow-y:auto;position:relative;}
#closeHistory {position:absolute;top:8px;right:8px;background:#333;color:#fff;border:1px solid #666;
font-size:12px;padding:6px 10px;cursor:pointer;border-radius:4px;}
#spyList {list-style:none;padding:0;margin:10px 0;}
#spyFilters {margin-bottom:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px;}
#spyFilters label {display:flex;align-items:center;gap:6px;font-size:12px;}
#spyFilters .actions {grid-column:1 / -1;display:flex;gap:8px;}
#spyFilters select, #spyFilters input {width:100%;background:#222;color:#eee;border:1px solid #666;padding:4px;}
#spyStats {margin:8px 0 0 0;font-size:12px;color:#aaa;}
#spyGraphSection {position:sticky;top:0;margin:10px 0;background:#222;padding:10px;z-index:10;display:none;}
#spyGraphSection h4 {margin:0 0 8px 0;}
`);
// =========================
// Menu + Popup
// =========================
function createMenu() {
if (document.getElementById("spyMenu")) return;
const menu = document.createElement("div");
menu.id = "spyMenu";
menu.innerHTML = `
<div id="spyMenuHeader">
<span id="spyMenuHeaderTitle">Espionnages reçus</span>
<button id="toggleSpyMenu" title="Réduire/Ouvrir">⮞</button>
</div>
<div id="spyMenuBody">
<button id="openSpyPopup" class="spy-btn">Consulter les espionnages</button>
<div id="spyTop">
<h4>Top 20 Espions</h4>
<ul id="spyRanking"></ul>
</div>
<button id="refreshSpy" class="spy-btn">Rafraîchir</button>
<button id="clearSpy" class="spy-btn">Effacer historique</button>
<button id="exportSpy" class="spy-btn">Exporter CSV</button>
<button id="importSpy" class="spy-btn">Importer CSV</button>
<input type="file" id="importFile" accept=".csv" style="display:none">
<button id="verifySpy" class="spy-btn">Vérifier cohérence</button>
<div id="spyHistoryPopup">
<div id="spyHistoryContent">
<button id="closeHistory">Fermer ✖</button>
<h3>Historique des espionnages</h3>
<div id="spyStats"></div>
<div id="spyFilters">
<label>De: <input type="datetime-local" id="filterStart"></label>
<label>À: <input type="datetime-local" id="filterEnd"></label>
<label>Heure début: <input type="time" id="filterStartHour"></label>
<label>Heure fin: <input type="time" id="filterEndHour"></label>
<label>Espion: <select id="filterPlayer"><option value="">Tous</option></select></label>
<label>Planète espion: <select id="filterPlanetEspion"><option value="">Toutes</option></select></label>
<label>Planète espionnée: <select id="filterPlanetEspionnee"><option value="">Toutes</option></select></label>
<div class="actions">
<button id="applyFilters" class="spy-btn">Appliquer</button>
<button id="resetFilters" class="spy-btn">Réinitialiser</button>
</div>
</div>
<div id="spyGraphSection">
<h4 id="spyGraphTitle"></h4>
<canvas id="spyGraphCanvas" width="780" height="250"></canvas>
</div>
<ul id="spyList"></ul>
</div>
</div>
</div>
`;
document.body.appendChild(menu);
const collapsed = localStorage.getItem("spyMenuCollapsed") === "true";
if (collapsed) {
menu.classList.add("collapsed");
document.getElementById("toggleSpyMenu").textContent = "⮜";
}
// Events
document.getElementById("toggleSpyMenu").addEventListener("click", () => {
menu.classList.toggle("collapsed");
const isCollapsed = menu.classList.contains("collapsed");
document.getElementById("toggleSpyMenu").textContent = isCollapsed ? "⮜" : "⮞";
localStorage.setItem("spyMenuCollapsed", String(isCollapsed));
});
document.getElementById("openSpyPopup").addEventListener("click", () => {
document.getElementById("spyHistoryPopup").style.display = "flex";
renderHistory();
});
document.getElementById("closeHistory").addEventListener("click", () => {
document.getElementById("spyHistoryPopup").style.display = "none";
});
document.getElementById("refreshSpy").addEventListener("click", () => {
parseMessages();
showNotification("Espionnages mis à jour ✅");
});
document.getElementById("clearSpy").addEventListener("click", clearHistory);
document.getElementById("exportSpy").addEventListener("click", exportCSV);
document.getElementById("importSpy").addEventListener("click", () => {
document.getElementById("importFile").click();
});
document.getElementById("importFile").addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) importCSV(file);
});
document.getElementById("verifySpy").addEventListener("click", verifyConsistency);
document.getElementById("applyFilters").addEventListener("click", applyFilters);
document.getElementById("resetFilters").addEventListener("click", resetFilters);
document.getElementById("filterPlayer").addEventListener("change", populateFilters);
}
// =========================
// Notifications
// =========================
function showNotification(msg) {
let notif = document.getElementById("spyNotification");
if (!notif) {
notif = document.createElement("div");
notif.id = "spyNotification";
document.body.appendChild(notif);
}
notif.textContent = msg;
notif.style.opacity = "1";
setTimeout(() => { notif.style.opacity = "0"; }, 2000);
}
// =========================
// Export / Import CSV (par univers/joueur)
// =========================
function exportCSV() {
let csv = "Date;Heure;Joueur;Planète Espion;Planète Espionnée\n";
history.forEach(e => {
csv += `${e.date};${e.hour};${e.player};${e.planetEspion};${e.planetEspionnee}\n`;
});
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const HH = String(now.getHours()).padStart(2, '0');
const MM = String(now.getMinutes()).padStart(2, '0');
const filename = `spiesExport-${getUniverseKey()}-${getPlayerName()}-${yyyy}${mm}${dd}-${HH}${MM}.csv`;
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename; a.click();
URL.revokeObjectURL(url);
showNotification(`Export CSV créé (${filename})`);
}
function importCSV(file) {
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
const lines = text.trim().split("\n");
const header = lines[0].replace(/^\uFEFF/, "");
const cols = header.split(";");
const expected = ["Date","Heure","Joueur","Planète Espion","Planète Espionnée"];
const isValid = expected.every((c, i) => (cols[i] || "").trim() === c);
if (!isValid) {
showNotification("Format CSV invalide ⚠️");
return;
}
const before = history.length;
lines.slice(1).forEach(line => {
const parts = line.split(";");
if (parts.length < 5) return;
const [date, hour, player, planetEspion, planetEspionnee] = parts.map(x => (x || "").trim());
if (!date || !hour || !player) return;
const exists = history.some(e =>
e.date === date &&
e.hour === hour &&
e.player === player &&
e.planetEspion === planetEspion &&
e.planetEspionnee === planetEspionnee
);
if (!exists) history.push({ date, hour, player, planetEspion, planetEspionnee });
});
saveHistory();
renderHistory();
const added = history.length - before;
showNotification(added > 0 ? `Import CSV terminé ✅ (+${added})` : "Import CSV terminé ✅ (aucun nouvel enregistrement)");
};
reader.readAsText(file, "UTF-8");
}
// =========================
// Historique: effacer / cohérence
// =========================
function clearHistory() {
const confirmation = confirm("Vous allez effacer l’historique pour cet univers et ce joueur. Continuer ?");
if (!confirmation) return;
history = [];
saveHistory();
renderHistory();
const section = document.getElementById("spyGraphSection");
if (section) section.style.display = "none";
if (spyChart) { spyChart.destroy(); spyChart = null; }
showNotification("Historique effacé 🗑️");
}
// Uniformisation des noms par planète espion
function verifyConsistency() {
const planetGroups = {};
history.forEach(entry => {
const key = entry.planetEspion || "??";
if (!planetGroups[key]) planetGroups[key] = [];
planetGroups[key].push(entry);
});
let changes = 0;
Object.values(planetGroups).forEach(entries => {
const playersSet = new Set(entries.map(e => e.player));
if (playersSet.size > 1) {
const latest = entries.reduce((a, b) => {
const da = toAbsoluteDate(a.date, a.hour);
const db = toAbsoluteDate(b.date, b.hour);
return (da && db && da > db) ? a : b;
});
const latestName = latest.player;
history.forEach(e => {
if (entries.some(x => x.player === e.player) && e.player !== latestName) {
e.player = latestName;
changes++;
}
});
}
});
saveHistory();
renderHistory();
showNotification(changes > 0
? `Vérification terminée ✅ (${changes} corrections appliquées)`
: "Vérification terminée ✅ (aucune correction)");
}
// =========================
// Rendu + Top20 + Filtres
// =========================
function renderHistory() {
const spyList = document.getElementById("spyList");
const spyRanking = document.getElementById("spyRanking");
const stats = document.getElementById("spyStats");
if (!spyList) return;
const sorted = history.slice().sort((a,b) => {
const da = toAbsoluteDate(a.date, a.hour);
const db = toAbsoluteDate(b.date, b.hour);
return db - da;
});
if (stats) stats.textContent = `Univers: ${getUniverseKey()} | Joueur: ${getPlayerName()} | Total enregistrements: ${history.length}`;
spyList.innerHTML = "";
sorted.forEach(entry => {
const li = document.createElement("li");
const playerLink = document.createElement("a");
playerLink.href = "#";
playerLink.textContent = entry.player;
playerLink.style.color = "#4FC3F7";
playerLink.addEventListener("click", (e) => {
e.preventDefault();
showGraph(entry.player, sorted); // graphe par joueur sur la liste affichée
});
li.textContent = `${entry.date} ${entry.hour} - `;
li.appendChild(playerLink);
li.appendChild(document.createTextNode(` (de ${entry.planetEspion} → ${entry.planetEspionnee})`));
spyList.appendChild(li);
});
if (spyRanking) {
spyRanking.innerHTML = "";
const counts = {};
history.forEach(e => { counts[e.player] = (counts[e.player] || 0) + 1; });
Object.entries(counts)
.sort((a,b) => b[1] - a[1])
.slice(0,20)
.forEach(([player,count]) => {
const li = document.createElement("li");
const link = document.createElement("a");
link.href = "#";
link.textContent = `${player} (${count})`;
link.style.color = "#FFD54F";
link.addEventListener("click", (e) => {
e.preventDefault();
document.getElementById("spyHistoryPopup").style.display = "flex";
populateFilters();
document.getElementById("filterPlayer").value = player;
applyFilters(); // graphe filtré (spécifique si joueur, sinon global)
});
li.appendChild(link);
spyRanking.appendChild(li);
});
}
populateFilters();
}
function populateFilters() {
const playerSelect = document.getElementById("filterPlayer");
const planetEspionSelect = document.getElementById("filterPlanetEspion");
const planetEspionneeSelect = document.getElementById("filterPlanetEspionnee");
if (!playerSelect || !planetEspionSelect || !planetEspionneeSelect) return;
const currentPlayer = playerSelect.value;
playerSelect.innerHTML = "<option value=''>Tous</option>";
planetEspionSelect.innerHTML = "<option value=''>Toutes</option>";
planetEspionneeSelect.innerHTML = "<option value=''>Toutes</option>";
const players = [...new Set(history.map(e => e.player))].sort();
players.forEach(p => {
const opt = document.createElement("option");
opt.value = p; opt.textContent = p;
playerSelect.appendChild(opt);
});
// Planètes espion dépendantes du joueur sélectionné
const planetsEspion = [...new Set(
(currentPlayer ? history.filter(e => e.player === currentPlayer) : history)
.map(e => e.planetEspion)
)].sort();
planetsEspion.forEach(p => {
const opt = document.createElement("option");
opt.value = p; opt.textContent = p;
planetEspionSelect.appendChild(opt);
});
// Planètes espionnées: globales (peut être rendu dépendant si souhaité)
const planetsEspionnee = [...new Set(history.map(e => e.planetEspionnee))].sort();
planetsEspionnee.forEach(p => {
const opt = document.createElement("option");
opt.value = p; opt.textContent = p;
planetEspionneeSelect.appendChild(opt);
});
if (currentPlayer && players.includes(currentPlayer)) {
playerSelect.value = currentPlayer;
}
}
// =========================
// Application des filtres + graphe
// =========================
function applyFilters() {
const startDateVal = document.getElementById("filterStart").value;
const endDateVal = document.getElementById("filterEnd").value;
const startHourVal = document.getElementById("filterStartHour").value;
const endHourVal = document.getElementById("filterEndHour").value;
const player = document.getElementById("filterPlayer").value;
const planetEspion = document.getElementById("filterPlanetEspion").value;
const planetEspionnee = document.getElementById("filterPlanetEspionnee").value;
let filtered = history.slice();
if (startDateVal) {
const start = new Date(startDateVal);
filtered = filtered.filter(e => {
const abs = toAbsoluteDate(e.date, e.hour);
return abs && abs >= start;
});
}
if (endDateVal) {
const end = new Date(endDateVal);
filtered = filtered.filter(e => {
const abs = toAbsoluteDate(e.date, e.hour);
return abs && abs <= end;
});
}
if (startHourVal || endHourVal) {
const s = startHourVal ? normalizeHour(startHourVal) : null;
const t = endHourVal ? normalizeHour(endHourVal) : null;
filtered = filtered.filter(e => {
const h = normalizeHour(e.hour.slice(0,5)); // HH:MM
if (s && t) {
return s <= t ? (h >= s && h <= t) : (h >= s || h <= t);
} else if (s) {
return h >= s;
} else if (t) {
return h <= t;
} else {
return true;
}
});
}
if (player) filtered = filtered.filter(e => e.player === player);
if (planetEspion) filtered = filtered.filter(e => e.planetEspion === planetEspion);
if (planetEspionnee) filtered = filtered.filter(e => e.planetEspionnee === planetEspionnee);
renderFilteredHistory(filtered);
const stats = document.getElementById("spyStats");
if (stats) stats.textContent = `Univers: ${getUniverseKey()} | Joueur: ${getPlayerName()} | Total: ${history.length} | Après filtre: ${filtered.length}`;
// Graphe toujours affiché (spécifique si joueur sélectionné, sinon global sur la liste filtrée)
showGraph(player || null, filtered);
}
function resetFilters() {
document.getElementById("filterStart").value = "";
document.getElementById("filterEnd").value = "";
document.getElementById("filterStartHour").value = "";
document.getElementById("filterEndHour").value = "";
document.getElementById("filterPlayer").value = "";
document.getElementById("filterPlanetEspion").value = "";
document.getElementById("filterPlanetEspionnee").value = "";
renderHistory();
const section = document.getElementById("spyGraphSection");
if (section) section.style.display = "none";
if (spyChart) { spyChart.destroy(); spyChart = null; }
const stats = document.getElementById("spyStats");
if (stats) stats.textContent = `Univers: ${getUniverseKey()} | Joueur: ${getPlayerName()} | Total enregistrements: ${history.length}`;
}
function renderFilteredHistory(list) {
const spyList = document.getElementById("spyList");
if (!spyList) return;
spyList.innerHTML = "";
const sorted = list.slice().sort((a,b) => {
const da = toAbsoluteDate(a.date, a.hour);
const db = toAbsoluteDate(b.date, b.hour);
return db - da;
});
sorted.forEach(entry => {
const li = document.createElement("li");
const playerLink = document.createElement("a");
playerLink.href = "#";
playerLink.textContent = entry.player;
playerLink.style.color = "#4FC3F7";
playerLink.addEventListener("click", (e) => {
e.preventDefault();
showGraph(entry.player, list); // graphe basé sur la liste filtrée
});
li.textContent = `${entry.date} ${entry.hour} - `;
li.appendChild(playerLink);
li.appendChild(document.createTextNode(` (de ${entry.planetEspion} → ${entry.planetEspionnee})`));
spyList.appendChild(li);
});
}
// =========================
// Graphe horaire (global ou joueur)
// =========================
function showGraph(player, list) {
const section = document.getElementById("spyGraphSection");
const title = document.getElementById("spyGraphTitle");
const canvas = document.getElementById("spyGraphCanvas");
if (!section || !title || !canvas) return;
const hours = Array(24).fill(0);
const dataset = player ? list.filter(e => e.player === player) : list;
dataset.forEach(e => {
const m = e.hour.match(/(\d{2}):(\d{2}):(\d{2})/);
if (m) {
const hour = parseInt(m[1], 10);
if (!isNaN(hour)) hours[hour]++;
}
});
section.style.display = "block";
title.textContent = player
? `Espionnages de ${player} (après filtre)`
: `Espionnages (tous joueurs, après filtre)`;
if (spyChart) spyChart.destroy();
// eslint-disable-next-line no-undef
spyChart = new Chart(canvas, {
type: 'bar',
data: {
labels: [...Array(24).keys()].map(h => h + "h"),
datasets: [{ label: 'Nombre d’espionnages', data: hours, backgroundColor: '#FF9800' }]
},
options: { scales: { y: { beginAtZero: true } }, plugins: { legend: { display: false } } }
});
}
// =========================
// Parsing des messages (OGLight + regex fallback)
// =========================
function parseMessages() {
const rows = document.querySelectorAll(".messageContentWrapper");
rows.forEach(row => {
const titleEl = row.querySelector(".msgTitle");
const dateEl = row.querySelector(".msgDate");
const playerEl = row.querySelector(".msgContent .player, .tooltipHTML.player");
const titleText = titleEl ? titleEl.textContent : "";
if (!titleText || !/espionnage/i.test(titleText)) return;
const player = playerEl ? playerEl.textContent.trim() : "Inconnu";
const full = dateEl ? dateEl.textContent.trim() : "";
const { date, hour } = splitOgameDate(full);
let planetEspion = "??";
let planetEspionnee = "??";
// OGLight
const oglCoords = Array.from(row.querySelectorAll(".msgContent a span"))
.map(s => (s.textContent || "").trim())
.filter(t => /\[\d+:\d+:\d+\]/.test(t));
if (oglCoords.length >= 2) {
planetEspion = oglCoords[0];
planetEspionnee = oglCoords[1];
} else if (oglCoords.length === 1) {
planetEspion = oglCoords[0];
} else {
// Fallback regex
const text = row.textContent || "";
const regexCoords = (text.match(/\[\d+:\d+:\d+\]/g) || []).map(s => s.trim());
if (regexCoords.length >= 2) {
planetEspion = regexCoords[0];
planetEspionnee = regexCoords[1];
} else if (regexCoords.length === 1) {
planetEspion = regexCoords[0];
}
if (planetEspion === planetEspionnee && regexCoords.length >= 2) {
planetEspionnee = regexCoords[1];
}
}
const exists = history.some(e =>
e.date === date &&
e.hour === hour &&
e.player === player &&
e.planetEspion === planetEspion &&
e.planetEspionnee === planetEspionnee
);
if (!exists) {
history.push({ date, hour, player, planetEspion, planetEspionnee });
}
});
saveHistory();
renderHistory();
}
// =========================
// Utilitaires
// =========================
function normalizeHour(hhmm) {
const m = hhmm.match(/(\d{1,2}):(\d{2})/);
if (!m) return hhmm;
const HH = String(m[1]).padStart(2, "0");
const MM = m[2];
return `${HH}:${MM}`;
}
function toAbsoluteDate(dateStr, timeStr) {
const dm = dateStr.match(/(\d{2})\.(\d{2})\.(\d{4})/);
const tm = timeStr.match(/(\d{2}):(\d{2}):(\d{2})/);
if (!dm || !tm) return null;
const [ , dd, mm, yyyy ] = dm;
const [ , HH, MM, SS ] = tm;
return new Date(Number(yyyy), Number(mm)-1, Number(dd), Number(HH), Number(MM), Number(SS));
}
function splitOgameDate(str) {
const m = str.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}:\d{2})/);
if (!m) return { date: "??", hour: "??" };
return { date: m[1], hour: m[2] };
}
// =========================
// Chargement (version stable)
// =========================
window.addEventListener("load", () => {
createMenu();
renderHistory();
parseMessages();
showNotification("Espionnages analysés au chargement ✅");
});
})();