Tileman.io Chat

A polished chat overlay for Tileman.io. Fixed loading screen crashes.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
    }
})();