Greasy Fork is available in English.
A polished chat overlay for Tileman.io. Fixed loading screen crashes.
// ==UserScript==
// @name Tileman.io Chat
// @namespace http://tampermonkey.net/
// @version 1.1.2
// @description A polished chat overlay for Tileman.io. Fixed loading screen crashes.
// @author Ech0
// @copyright 2025, Ech0
// @license MIT
// @match https://tileman.io/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// CONSTANTS & TOPICS
const GLOBAL_TOPIC = "tileman_chat_global_v2";
let lobbyTopic = null;
let currentChannel = "global";
let eventSource = null;
// Helper to generate a unique, non-guessable topic string based on the server address
function hashTopicString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return "tileman_chat_lobby_" + Math.abs(hash);
}
// SAFE INTERCEPTION: We intercept the native WebSocket to see the server URL.
// This entirely avoids touching the game's fragile Socket.IO code, fixing the loading screen crashes!
const OriginalWebSocket = window.WebSocket;
window.WebSocket = function(url, protocols) {
try {
// URL looks like: "wss://eu1.tileman.io/socket.io/?EIO=4&transport=websocket..."
const parsedUrl = new URL(url);
// Only trigger if it's the main game socket
if (parsedUrl.hostname.includes('tileman')) {
const rawRoom = parsedUrl.hostname + parsedUrl.pathname;
lobbyTopic = hashTopicString(rawRoom);
// Update Lobby select option text to reflect active server connection
const select = document.getElementById('tileman-chat-channel-select');
if (select) {
const lobbyOption = select.querySelector('option[value="lobby"]');
if (lobbyOption) {
lobbyOption.textContent = "Lobby Chat (Connected)";
}
}
// If currently on Lobby channel, switch to it to establish active listener
if (currentChannel === "lobby") {
switchChannel("lobby");
}
}
} catch(e) {
console.error("[Chat] WebSocket parsing error:", e);
}
// Return the actual game websocket so the game loads perfectly normally
return new OriginalWebSocket(url, protocols);
};
window.WebSocket.prototype = OriginalWebSocket.prototype;
// Determine the active ntfy topic name based on selection
function getTopicName(channelType) {
if (channelType === "lobby") {
return lobbyTopic || "tileman_chat_lobby_pending";
}
return GLOBAL_TOPIC;
}
// Load message history from ntfy's cache
function loadHistory(topic) {
fetch(`https://ntfy.sh/${topic}/json?poll=1`)
.then(response => response.text())
.then(text => {
const lines = text.trim().split('\n');
lines.forEach(line => {
if (!line) return;
try {
const parsedData = JSON.parse(line);
const payload = JSON.parse(parsedData.message);
if (payload.username && payload.text) {
addMessageToChat(payload.username, payload.text, true);
}
} catch (err) {
// Ignore corrupted history lines
}
});
})
.catch(err => console.error("[Chat] Failed to load history:", err));
}
// Establish real-time listener for live messages
function connectLiveStream(topic) {
if (eventSource) {
eventSource.close();
}
try {
eventSource = new EventSource(`https://ntfy.sh/${topic}/sse`);
eventSource.onmessage = function(event) {
try {
const rawData = JSON.parse(event.data);
const payload = JSON.parse(rawData.message);
if (payload.username && payload.text) {
addMessageToChat(payload.username, payload.text, false);
}
} catch (err) {
// Ignore live malformed data
}
};
eventSource.onerror = function() {
setTimeout(() => connectLiveStream(topic), 5000);
};
} catch (e) {
console.error("[Chat] EventSource failed:", e);
}
}
// Switch channels cleanly
function switchChannel(channelType) {
currentChannel = channelType;
const log = document.getElementById('tileman-chat-log');
if (log) log.innerHTML = '';
const topic = getTopicName(channelType);
loadHistory(topic);
connectLiveStream(topic);
}
// Broadcast message payload
function sendMessage(text) {
const username = localStorage.getItem('n') || 'Anonymous';
const topic = getTopicName(currentChannel);
const payload = {
username: username,
text: text
};
fetch(`https://ntfy.sh/${topic}`, {
method: 'POST',
body: JSON.stringify(payload)
}).catch(err => console.error("[Chat] Send error:", err));
}
// UI Assembly
function initUI() {
if (!document.body) {
setTimeout(initUI, 100);
return;
}
const container = document.createElement('div');
container.id = 'tileman-chat-container';
container.style.cssText = `
position: absolute;
bottom: 30px;
left: 20px;
width: 340px;
height: 260px;
background: rgba(18, 18, 18, 0.92);
color: #ffffff;
border: 1px solid #333333;
display: flex;
flex-direction: column;
font-family: Arial, sans-serif;
z-index: 999999;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
`;
container.innerHTML = `
<div id="tileman-chat-header" style="background: #252525; padding: 6px 10px; cursor: move; font-weight: bold; font-size: 11px; display: flex; align-items: center; justify-content: space-between; border-top-left-radius: 7px; border-top-right-radius: 7px; user-select: none; border-bottom: 1px solid #2e2e2e;">
<span style="letter-spacing: 0.5px;">CHAT SYSTEM</span>
<select id="tileman-chat-channel-select" style="background: #111; color: #fff; border: 1px solid #444; font-size: 10px; border-radius: 4px; padding: 2px 4px; outline: none; cursor: pointer;">
<option value="global">Global Chat</option>
<option value="lobby">Lobby Chat (Connecting...)</option>
</select>
</div>
<div id="tileman-chat-log" style="flex: 1; overflow-y: auto; padding: 10px; font-size: 11px; line-height: 1.4; color: #e0e0e0; font-family: monospace;"></div>
<div style="display: flex; border-top: 1px solid #2a2a2a; padding: 6px; background: #141414; border-bottom-left-radius: 7px; border-bottom-right-radius: 7px;">
<input id="tileman-chat-input" type="text" placeholder="Type message..." style="flex: 1; background: #080808; color: #fff; border: 1px solid #2e2e2e; padding: 5px 8px; font-size: 11px; border-radius: 4px; outline: none; font-family: sans-serif;" />
<button id="tileman-chat-send" style="background: #2196f3; color: white; border: none; padding: 5px 14px; margin-left: 6px; cursor: pointer; font-size: 11px; border-radius: 4px; font-weight: bold; transition: background 0.2s;">SEND</button>
</div>
`;
document.body.appendChild(container);
const header = document.getElementById('tileman-chat-header');
makeDraggable(container, header);
const select = document.getElementById('tileman-chat-channel-select');
select.addEventListener('change', (e) => {
switchChannel(e.target.value);
});
const input = document.getElementById('tileman-chat-input');
const sendBtn = document.getElementById('tileman-chat-send');
function handleSend() {
const text = input.value.trim();
if (text) {
sendMessage(text);
input.value = "";
}
}
sendBtn.addEventListener('click', handleSend);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
handleSend();
e.stopPropagation();
}
});
switchChannel("global");
}
// Add message element to scroll box
function addMessageToChat(senderName, text, isHistorical = false) {
const log = document.getElementById('tileman-chat-log');
if (!log) return;
const msgEl = document.createElement('div');
msgEl.style.marginBottom = '6px';
msgEl.style.wordBreak = 'break-all';
if (isHistorical) {
msgEl.style.opacity = '0.65';
}
const nameSpan = document.createElement('span');
nameSpan.style.fontWeight = 'bold';
const myName = localStorage.getItem('n') || 'Anonymous';
nameSpan.style.color = (senderName === myName) ? "#4fc3f7" : "#81c784";
nameSpan.textContent = `<${senderName}> `;
const textSpan = document.createElement('span');
textSpan.textContent = text;
msgEl.appendChild(nameSpan);
msgEl.appendChild(textSpan);
log.appendChild(msgEl);
log.scrollTop = log.scrollHeight;
}
function makeDraggable(el, header) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
header.onmousedown = dragMouseDown;
function dragMouseDown(e) {
if (e.target.tagName === 'SELECT') return;
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
el.style.top = (el.offsetTop - pos2) + "px";
el.style.left = (el.offsetLeft - pos1) + "px";
el.style.bottom = 'auto';
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initUI);
} else {
initUI();
}
})();