Tileman.io Chat

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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