diff --git a/resources/chat/client.js b/resources/chat/client.js
new file mode 100644
index 0000000..8c5a51f
--- /dev/null
+++ b/resources/chat/client.js
@@ -0,0 +1,579 @@
+// ============================================================================
+// CUSTOM CHAT UI - Client Side
+// Modern glossy UI matching MD Revolution menu style
+// ============================================================================
+
+// ============================================================================
+// UI THEME - Dark Glassmorphism (matching mod menu)
+// ============================================================================
+const UI = {
+ // Background colors
+ bgDark: { r: 13, g: 17, b: 23 }, // Deep dark background
+ bgPanel: { r: 22, g: 27, b: 34 }, // Panel background
+ bgHover: { r: 33, g: 38, b: 45 }, // Hover state
+ bgSelected: { r: 45, g: 50, b: 58 }, // Selected item
+ bgMessage: { r: 18, g: 22, b: 28 }, // Message background
+
+ // Accent colors - clean cyan
+ accent: { r: 0, g: 212, b: 255 }, // Primary accent (cyan)
+ accentDim: { r: 0, g: 150, b: 180 }, // Dimmed accent
+ accentGlow: { r: 0, g: 180, b: 220 }, // Glow effect
+
+ // Text colors
+ textPrimary: { r: 230, g: 237, b: 243 }, // Primary text (almost white)
+ textSecondary: { r: 139, g: 148, b: 158 }, // Secondary text (gray)
+ textMuted: { r: 88, g: 96, b: 105 }, // Muted text
+
+ // Status colors
+ success: { r: 63, g: 185, b: 80 }, // Green for joins
+ error: { r: 248, g: 81, b: 73 }, // Red for leaves
+ warning: { r: 210, g: 153, b: 34 }, // Yellow/gold for warnings
+ info: { r: 88, g: 166, b: 255 }, // Blue for info
+
+ // Chat specific colors
+ playerName: { r: 0, g: 212, b: 255 }, // Player name color (cyan)
+ chatWhite: { r: 255, g: 255, b: 255 }, // Normal chat
+ chatAction: { r: 200, g: 100, b: 255 }, // /me actions (purple)
+ chatWhisper: { r: 255, g: 255, b: 100 }, // Private messages (yellow)
+ chatShout: { r: 255, g: 100, b: 100 }, // Shout (red)
+ chatOOC: { r: 150, g: 150, b: 150 }, // OOC (gray)
+ chatSystem: { r: 100, g: 200, b: 255 }, // System messages (light blue)
+ chatLocal: { r: 200, g: 255, b: 200 }, // Local chat (light green)
+
+ // Border colors
+ border: { r: 48, g: 54, b: 61 }, // Subtle border
+ borderFocus: { r: 0, g: 212, b: 255 } // Focused border (accent)
+};
+
+// ============================================================================
+// CHAT STATE
+// ============================================================================
+let chatMessages = [];
+const maxMessages = 50;
+let chatVisible = true;
+let chatInputActive = false;
+let chatInputText = "";
+let chatScrollOffset = 0;
+let chatFont = null;
+
+// Animation state
+let animTime = 0;
+let chatFadeAlpha = 1.0;
+let lastMessageTime = 0;
+let chatFadeDelay = 10000; // 10 seconds before fade
+let chatFadeSpeed = 0.02;
+
+// Welcome/Leave notification queue
+let notifications = [];
+let notificationDuration = 5000; // 5 seconds
+
+// Chat dimensions
+const chat = {
+ x: 25,
+ y: 200,
+ width: 550,
+ messageHeight: 22,
+ maxVisibleMessages: 12,
+ padding: 12,
+ inputHeight: 36,
+ borderRadius: 4
+};
+
+// ============================================================================
+// RESOURCE START - Load font
+// ============================================================================
+addEventHandler("OnResourceStart", function(event, resource) {
+ if (resource == thisResource) {
+ try {
+ chatFont = lucasFont;
+ console.log("[Chat] Custom chat UI initialized");
+ } catch(e) {
+ console.log("[Chat] Font load error: " + e);
+ }
+ }
+});
+
+// ============================================================================
+// NETWORK EVENTS - Receive messages from server
+// ============================================================================
+addNetworkHandler("chatMessage", function(messageData) {
+ addChatMessage(messageData.text, messageData.type || "normal", messageData.playerName || null);
+});
+
+addNetworkHandler("playerJoined", function(data) {
+ addNotification(data.name + " joined the server", "join");
+ addChatMessage(data.name + " has joined the server", "system");
+});
+
+addNetworkHandler("playerLeft", function(data) {
+ let reason = data.reason || "Disconnected";
+ addNotification(data.name + " left the server", "leave");
+ addChatMessage(data.name + " has left the server (" + reason + ")", "system");
+});
+
+// ============================================================================
+// CHAT FUNCTIONS
+// ============================================================================
+
+function addChatMessage(text, type, playerName) {
+ let message = {
+ text: text,
+ type: type || "normal",
+ playerName: playerName,
+ timestamp: Date.now(),
+ alpha: 1.0
+ };
+
+ chatMessages.push(message);
+
+ // Limit messages
+ if (chatMessages.length > maxMessages) {
+ chatMessages.shift();
+ }
+
+ // Reset fade timer
+ lastMessageTime = Date.now();
+ chatFadeAlpha = 1.0;
+
+ // Auto-scroll to bottom
+ let totalMessages = chatMessages.length;
+ if (totalMessages > chat.maxVisibleMessages) {
+ chatScrollOffset = totalMessages - chat.maxVisibleMessages;
+ }
+}
+
+function addNotification(text, type) {
+ notifications.push({
+ text: text,
+ type: type, // "join" or "leave"
+ timestamp: Date.now(),
+ alpha: 1.0,
+ y: 0 // Will be animated
+ });
+}
+
+function getMessageColor(type) {
+ switch(type) {
+ case "action": return UI.chatAction;
+ case "whisper": return UI.chatWhisper;
+ case "shout": return UI.chatShout;
+ case "ooc": return UI.chatOOC;
+ case "system": return UI.chatSystem;
+ case "local": return UI.chatLocal;
+ case "join": return UI.success;
+ case "leave": return UI.error;
+ default: return UI.chatWhite;
+ }
+}
+
+// ============================================================================
+// DRAWING FUNCTIONS
+// ============================================================================
+
+function drawRect(x, y, w, h, colour) {
+ try {
+ let pos = new Vec2(x, y);
+ let size = new Vec2(w, h);
+ graphics.drawRectangle(null, pos, size, colour, colour, colour, colour);
+ } catch(e) {}
+}
+
+function drawGradientRect(x, y, w, h, colourLeft, colourRight) {
+ try {
+ let pos = new Vec2(x, y);
+ let size = new Vec2(w, h);
+ graphics.drawRectangle(null, pos, size, colourLeft, colourRight, colourLeft, colourRight);
+ } catch(e) {}
+}
+
+function drawGradientRectV(x, y, w, h, colourTop, colourBottom) {
+ try {
+ let pos = new Vec2(x, y);
+ let size = new Vec2(w, h);
+ graphics.drawRectangle(null, pos, size, colourTop, colourTop, colourBottom, colourBottom);
+ } catch(e) {}
+}
+
+function drawText(text, x, y, colour, size) {
+ if (chatFont != null) {
+ try {
+ let pos = new Vec2(x, y);
+ chatFont.render(text, pos, chat.width - 20, 0.0, 0.0, size || 11, colour, false, false, false, true);
+ } catch(e) {}
+ }
+}
+
+// ============================================================================
+// RENDER CHAT UI
+// ============================================================================
+addEventHandler("OnDrawnHUD", function(event) {
+ animTime += 0.016; // ~60fps
+
+ let screenWidth = 1920;
+ let screenHeight = 1080;
+ try {
+ screenWidth = game.width || 1920;
+ screenHeight = game.height || 1080;
+ } catch(e) {}
+
+ // Update fade
+ if (Date.now() - lastMessageTime > chatFadeDelay && !chatInputActive) {
+ chatFadeAlpha = Math.max(0.3, chatFadeAlpha - chatFadeSpeed);
+ }
+
+ // Draw notifications first (top right)
+ drawNotifications(screenWidth, screenHeight);
+
+ // Skip chat drawing if hidden
+ if (!chatVisible && !chatInputActive) return;
+
+ let alpha = Math.floor(255 * chatFadeAlpha);
+ if (chatInputActive) alpha = 255;
+
+ // Calculate chat area
+ let chatAreaHeight = chat.maxVisibleMessages * chat.messageHeight + chat.padding * 2;
+ let totalHeight = chatAreaHeight + (chatInputActive ? chat.inputHeight + 8 : 0);
+
+ // ========================================
+ // MAIN CHAT PANEL - Glossy background
+ // ========================================
+
+ // Outer glow effect (subtle)
+ let glowAlpha = Math.floor(30 * chatFadeAlpha);
+ let glowCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, glowAlpha);
+ drawRect(chat.x - 2, chat.y - 2, chat.width + 4, chatAreaHeight + 4, glowCol);
+
+ // Main panel background with transparency
+ let bgAlpha = Math.floor(200 * chatFadeAlpha);
+ let panelBg = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, bgAlpha);
+ drawRect(chat.x, chat.y, chat.width, chatAreaHeight, panelBg);
+
+ // Glassmorphism overlay (subtle gradient)
+ let glossTop = toColour(255, 255, 255, Math.floor(8 * chatFadeAlpha));
+ let glossBottom = toColour(255, 255, 255, 0);
+ drawGradientRectV(chat.x, chat.y, chat.width, 30, glossTop, glossBottom);
+
+ // Top accent line
+ let accentAlpha = Math.floor(255 * chatFadeAlpha);
+ let accentCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, accentAlpha);
+ drawRect(chat.x, chat.y, chat.width, 2, accentCol);
+
+ // Side accent line (left)
+ let sideAccent = toColour(UI.accent.r, UI.accent.g, UI.accent.b, Math.floor(150 * chatFadeAlpha));
+ drawRect(chat.x, chat.y, 2, chatAreaHeight, sideAccent);
+
+ // ========================================
+ // CHAT HEADER
+ // ========================================
+ let headerHeight = 28;
+ let headerBg = toColour(UI.bgPanel.r, UI.bgPanel.g, UI.bgPanel.b, bgAlpha);
+ drawRect(chat.x, chat.y + 2, chat.width, headerHeight, headerBg);
+
+ // Header text
+ let headerTextCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, alpha);
+ drawText("CHAT", chat.x + chat.padding, chat.y + 8, headerTextCol, 11);
+
+ // Online count (placeholder - can be updated via network)
+ let onlineCol = toColour(UI.textSecondary.r, UI.textSecondary.g, UI.textSecondary.b, alpha);
+ drawText("Press T to chat", chat.x + chat.width - 120, chat.y + 9, onlineCol, 9);
+
+ // Header bottom border
+ let headerBorder = toColour(UI.border.r, UI.border.g, UI.border.b, Math.floor(100 * chatFadeAlpha));
+ drawRect(chat.x + chat.padding, chat.y + headerHeight + 2, chat.width - chat.padding * 2, 1, headerBorder);
+
+ // ========================================
+ // CHAT MESSAGES
+ // ========================================
+ let messageStartY = chat.y + headerHeight + chat.padding;
+ let visibleCount = Math.min(chatMessages.length, chat.maxVisibleMessages);
+ let startIndex = Math.max(0, chatMessages.length - chat.maxVisibleMessages - chatScrollOffset);
+
+ for (let i = 0; i < visibleCount && startIndex + i < chatMessages.length; i++) {
+ let msg = chatMessages[startIndex + i];
+ let msgY = messageStartY + (i * chat.messageHeight);
+
+ // Message background (alternating subtle)
+ if (i % 2 === 0) {
+ let msgBgAlpha = Math.floor(30 * chatFadeAlpha);
+ let msgBg = toColour(UI.bgMessage.r, UI.bgMessage.g, UI.bgMessage.b, msgBgAlpha);
+ drawRect(chat.x + 4, msgY - 2, chat.width - 8, chat.messageHeight, msgBg);
+ }
+
+ // Get message color based on type
+ let msgColor = getMessageColor(msg.type);
+ let textCol = toColour(msgColor.r, msgColor.g, msgColor.b, alpha);
+
+ // Draw timestamp
+ let time = new Date(msg.timestamp);
+ let timeStr = "[" + padZero(time.getHours()) + ":" + padZero(time.getMinutes()) + "]";
+ let timeCol = toColour(UI.textMuted.r, UI.textMuted.g, UI.textMuted.b, Math.floor(alpha * 0.7));
+ drawText(timeStr, chat.x + chat.padding, msgY, timeCol, 9);
+
+ // Draw message text
+ let textX = chat.x + chat.padding + 55;
+
+ if (msg.playerName && msg.type === "normal") {
+ // Player name in accent color
+ let nameCol = toColour(UI.playerName.r, UI.playerName.g, UI.playerName.b, alpha);
+ drawText(msg.playerName + ":", textX, msgY, nameCol, 10);
+ // Message text
+ drawText(msg.text, textX + (msg.playerName.length * 7) + 12, msgY, textCol, 10);
+ } else {
+ drawText(msg.text, textX, msgY, textCol, 10);
+ }
+ }
+
+ // ========================================
+ // SCROLLBAR (if needed)
+ // ========================================
+ if (chatMessages.length > chat.maxVisibleMessages) {
+ let scrollbarX = chat.x + chat.width - 6;
+ let scrollbarY = messageStartY;
+ let scrollbarHeight = chat.maxVisibleMessages * chat.messageHeight;
+
+ // Scrollbar track
+ let trackCol = toColour(UI.bgHover.r, UI.bgHover.g, UI.bgHover.b, Math.floor(100 * chatFadeAlpha));
+ drawRect(scrollbarX, scrollbarY, 4, scrollbarHeight, trackCol);
+
+ // Scrollbar thumb
+ let thumbHeight = Math.max(20, (chat.maxVisibleMessages / chatMessages.length) * scrollbarHeight);
+ let thumbY = scrollbarY + ((startIndex / (chatMessages.length - chat.maxVisibleMessages)) * (scrollbarHeight - thumbHeight));
+ let thumbCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, Math.floor(200 * chatFadeAlpha));
+ drawRect(scrollbarX, thumbY, 4, thumbHeight, thumbCol);
+ }
+
+ // ========================================
+ // INPUT BOX (when active)
+ // ========================================
+ if (chatInputActive) {
+ let inputY = chat.y + chatAreaHeight + 6;
+
+ // Input background
+ let inputBg = toColour(UI.bgPanel.r, UI.bgPanel.g, UI.bgPanel.b, 240);
+ drawRect(chat.x, inputY, chat.width, chat.inputHeight, inputBg);
+
+ // Input border glow
+ let pulseAlpha = Math.floor(180 + Math.sin(animTime * 4) * 50);
+ let inputBorder = toColour(UI.accent.r, UI.accent.g, UI.accent.b, pulseAlpha);
+ drawRect(chat.x, inputY, chat.width, 2, inputBorder);
+ drawRect(chat.x, inputY + chat.inputHeight - 2, chat.width, 2, inputBorder);
+ drawRect(chat.x, inputY, 2, chat.inputHeight, inputBorder);
+ drawRect(chat.x + chat.width - 2, inputY, 2, chat.inputHeight, inputBorder);
+
+ // Input label
+ let labelCol = toColour(UI.textMuted.r, UI.textMuted.g, UI.textMuted.b, 255);
+ drawText("Say:", chat.x + chat.padding, inputY + 10, labelCol, 10);
+
+ // Input text with cursor
+ let cursorBlink = Math.floor(animTime * 2) % 2 === 0;
+ let inputTextCol = toColour(UI.textPrimary.r, UI.textPrimary.g, UI.textPrimary.b, 255);
+ let displayText = chatInputText + (cursorBlink ? "|" : "");
+ drawText(displayText, chat.x + chat.padding + 40, inputY + 10, inputTextCol, 10);
+ }
+
+ // ========================================
+ // BOTTOM PANEL FADE
+ // ========================================
+ let fadeHeight = 20;
+ let fadeTop = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, 0);
+ let fadeBottom = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, bgAlpha);
+ drawGradientRectV(chat.x, chat.y + chatAreaHeight - fadeHeight, chat.width, fadeHeight, fadeTop, fadeBottom);
+
+ // Bottom border
+ drawRect(chat.x, chat.y + chatAreaHeight - 1, chat.width, 1, accentCol);
+});
+
+// ========================================
+// DRAW NOTIFICATIONS (Join/Leave)
+// ========================================
+function drawNotifications(screenWidth, screenHeight) {
+ let notifX = screenWidth - 350;
+ let notifY = 100;
+ let notifHeight = 45;
+ let notifSpacing = 8;
+
+ // Update and draw each notification
+ for (let i = notifications.length - 1; i >= 0; i--) {
+ let notif = notifications[i];
+ let age = Date.now() - notif.timestamp;
+
+ // Remove old notifications
+ if (age > notificationDuration) {
+ notifications.splice(i, 1);
+ continue;
+ }
+
+ // Calculate alpha (fade in/out)
+ let fadeIn = Math.min(1, age / 200);
+ let fadeOut = Math.min(1, (notificationDuration - age) / 500);
+ notif.alpha = fadeIn * fadeOut;
+
+ // Calculate slide animation
+ let slideIn = Math.min(1, age / 300);
+ let slideX = notifX + (1 - slideIn) * 100;
+
+ let alpha = Math.floor(255 * notif.alpha);
+ let displayIndex = notifications.length - 1 - i;
+ let yPos = notifY + displayIndex * (notifHeight + notifSpacing);
+
+ // Notification background
+ let bgAlpha = Math.floor(220 * notif.alpha);
+ let bgCol = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, bgAlpha);
+ drawRect(slideX, yPos, 320, notifHeight, bgCol);
+
+ // Glossy top effect
+ let glossTop = toColour(255, 255, 255, Math.floor(15 * notif.alpha));
+ let glossBottom = toColour(255, 255, 255, 0);
+ drawGradientRectV(slideX, yPos, 320, 15, glossTop, glossBottom);
+
+ // Left accent bar based on type
+ let accentColor = notif.type === "join" ? UI.success : UI.error;
+ let accentBarCol = toColour(accentColor.r, accentColor.g, accentColor.b, alpha);
+ drawRect(slideX, yPos, 4, notifHeight, accentBarCol);
+
+ // Icon (+ for join, - for leave)
+ let iconText = notif.type === "join" ? "+" : "-";
+ let iconCol = toColour(accentColor.r, accentColor.g, accentColor.b, alpha);
+ drawText(iconText, slideX + 15, yPos + 12, iconCol, 18);
+
+ // Notification text
+ let textCol = toColour(UI.textPrimary.r, UI.textPrimary.g, UI.textPrimary.b, alpha);
+ drawText(notif.text, slideX + 40, yPos + 14, textCol, 11);
+
+ // Subtitle
+ let subtitleText = notif.type === "join" ? "Welcome to the server!" : "Goodbye!";
+ let subtitleCol = toColour(UI.textSecondary.r, UI.textSecondary.g, UI.textSecondary.b, Math.floor(alpha * 0.7));
+ drawText(subtitleText, slideX + 40, yPos + 28, subtitleCol, 9);
+
+ // Outer glow
+ let glowAlpha = Math.floor(40 * notif.alpha);
+ let glowCol = toColour(accentColor.r, accentColor.g, accentColor.b, glowAlpha);
+ drawRect(slideX - 2, yPos - 2, 324, notifHeight + 4, glowCol);
+ }
+}
+
+// ========================================
+// HELPER FUNCTIONS
+// ========================================
+function padZero(num) {
+ return num < 10 ? "0" + num : num.toString();
+}
+
+// ============================================================================
+// INPUT HANDLING
+// ============================================================================
+addEventHandler("OnKeyUp", function(event, key, scancode, mods) {
+ // T key to open chat
+ if (key === SDLK_t && !chatInputActive) {
+ chatInputActive = true;
+ chatInputText = "";
+ gui.showCursor(true, true);
+ event.preventDefault();
+ return;
+ }
+
+ // Escape to close chat
+ if (key === SDLK_ESCAPE && chatInputActive) {
+ chatInputActive = false;
+ chatInputText = "";
+ gui.showCursor(false, false);
+ event.preventDefault();
+ return;
+ }
+
+ // Enter to send message
+ if (key === SDLK_RETURN && chatInputActive) {
+ if (chatInputText.length > 0) {
+ // Send message to server
+ triggerNetworkEvent("chatSendMessage", chatInputText);
+ }
+ chatInputActive = false;
+ chatInputText = "";
+ gui.showCursor(false, false);
+ event.preventDefault();
+ return;
+ }
+
+ // Page up/down for scrolling
+ if (key === SDLK_PAGEUP && chatMessages.length > chat.maxVisibleMessages) {
+ chatScrollOffset = Math.min(chatScrollOffset + 3, chatMessages.length - chat.maxVisibleMessages);
+ chatFadeAlpha = 1.0;
+ lastMessageTime = Date.now();
+ }
+ if (key === SDLK_PAGEDOWN) {
+ chatScrollOffset = Math.max(0, chatScrollOffset - 3);
+ chatFadeAlpha = 1.0;
+ lastMessageTime = Date.now();
+ }
+});
+
+// Handle text input
+addEventHandler("OnCharacter", function(event, character) {
+ if (chatInputActive) {
+ chatInputText += character;
+ event.preventDefault();
+ }
+});
+
+addEventHandler("OnKeyDown", function(event, key, scancode, mods) {
+ if (!chatInputActive) return;
+
+ // Backspace
+ if (key === SDLK_BACKSPACE && chatInputText.length > 0) {
+ chatInputText = chatInputText.slice(0, -1);
+ event.preventDefault();
+ }
+});
+
+// ============================================================================
+// MOUSE WHEEL SCROLLING
+// ============================================================================
+addEventHandler("OnMouseWheel", function(event, x, y) {
+ if (chatMessages.length <= chat.maxVisibleMessages) return;
+
+ // Check if mouse is over chat area
+ let mouseX = gui.cursorPosition.x;
+ let mouseY = gui.cursorPosition.y;
+
+ if (mouseX >= chat.x && mouseX <= chat.x + chat.width &&
+ mouseY >= chat.y && mouseY <= chat.y + chat.maxVisibleMessages * chat.messageHeight + 50) {
+
+ if (y > 0) {
+ chatScrollOffset = Math.min(chatScrollOffset + 2, chatMessages.length - chat.maxVisibleMessages);
+ } else {
+ chatScrollOffset = Math.max(0, chatScrollOffset - 2);
+ }
+
+ chatFadeAlpha = 1.0;
+ lastMessageTime = Date.now();
+ }
+});
+
+// ============================================================================
+// PROCESS - Animation updates
+// ============================================================================
+addEventHandler("OnProcess", function(event) {
+ // Block game input while typing
+ if (chatInputActive) {
+ // Disable native chat
+ try {
+ chatInputEnabled = false;
+ } catch(e) {}
+ }
+});
+
+// ============================================================================
+// DISABLE DEFAULT CHAT
+// ============================================================================
+addEventHandler("OnResourceReady", function(event, resource) {
+ if (resource == thisResource) {
+ try {
+ // Disable default chat window
+ setChatWindowEnabled(false);
+ } catch(e) {
+ console.log("[Chat] Could not disable default chat: " + e);
+ }
+ }
+});
+
+console.log("[Chat] Client script loaded - Custom glossy UI enabled");
diff --git a/resources/chat/meta.xml b/resources/chat/meta.xml
new file mode 100644
index 0000000..8b83d87
--- /dev/null
+++ b/resources/chat/meta.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/resources/chat/server.js b/resources/chat/server.js
new file mode 100644
index 0000000..c426416
--- /dev/null
+++ b/resources/chat/server.js
@@ -0,0 +1,392 @@
+// ============================================================================
+// CHAT RESOURCE - Server Side
+// Enhanced chat system with custom UI, welcome/leave messages, and features
+// ============================================================================
+
+// Chat colors using toColour for integer format
+const COLOUR_WHITE = toColour(255, 255, 255, 255);
+const COLOUR_ACTION = toColour(200, 100, 255, 255);
+const COLOUR_WHISPER = toColour(255, 255, 100, 255);
+const COLOUR_SHOUT = toColour(255, 100, 100, 255);
+const COLOUR_OOC = toColour(150, 150, 150, 255);
+const COLOUR_ADMIN = toColour(255, 100, 100, 255);
+const COLOUR_SYSTEM = toColour(100, 200, 255, 255);
+const COLOUR_ORANGE = toColour(255, 200, 100, 255);
+const COLOUR_ERROR = toColour(255, 100, 100, 255);
+const COLOUR_GRAY = toColour(200, 200, 200, 255);
+const COLOUR_LOCAL = toColour(200, 255, 200, 255);
+const COLOUR_JOIN = toColour(63, 185, 80, 255);
+const COLOUR_LEAVE = toColour(248, 81, 73, 255);
+
+// Chat history (for logging purposes)
+let chatHistory = [];
+const maxHistory = 100;
+
+// Store last PM sender for reply function
+let lastPMSender = {};
+
+// ============================================================================
+// EVENTS
+// ============================================================================
+
+addEventHandler("OnResourceStart", function(event, resource) {
+ if (resource == thisResource) {
+ console.log("[Chat] Resource started - Custom chat system active");
+ }
+});
+
+// ============================================================================
+// PLAYER JOIN/LEAVE EVENTS
+// ============================================================================
+
+addEventHandler("OnPlayerJoined", function(event, client) {
+ // Broadcast join notification to all clients
+ let joinData = {
+ name: client.name
+ };
+
+ // Send to all connected clients
+ let clients = getClients();
+ for (let i = 0; i < clients.length; i++) {
+ triggerNetworkEvent("playerJoined", clients[i], joinData);
+ }
+
+ // Log to console
+ console.log("[Chat] " + client.name + " joined the server");
+
+ // Also send via native message for fallback
+ message(client.name + " has joined the server", COLOUR_JOIN);
+});
+
+addEventHandler("OnPlayerQuit", function(event, client, reason) {
+ // Get disconnect reason text
+ let reasonText = getDisconnectReason(reason);
+
+ // Broadcast leave notification to all remaining clients
+ let leaveData = {
+ name: client.name,
+ reason: reasonText
+ };
+
+ let clients = getClients();
+ for (let i = 0; i < clients.length; i++) {
+ if (clients[i].index !== client.index) {
+ triggerNetworkEvent("playerLeft", clients[i], leaveData);
+ }
+ }
+
+ // Log to console
+ console.log("[Chat] " + client.name + " left the server (" + reasonText + ")");
+
+ // Also send via native message for fallback
+ message(client.name + " has left the server (" + reasonText + ")", COLOUR_LEAVE);
+
+ // Clear PM data
+ delete lastPMSender[client.index];
+});
+
+function getDisconnectReason(reason) {
+ switch(reason) {
+ case 0: return "Disconnected";
+ case 1: return "Timed Out";
+ case 2: return "Kicked";
+ case 3: return "Banned";
+ case 4: return "Connection Lost";
+ default: return "Unknown";
+ }
+}
+
+// ============================================================================
+// CHAT MESSAGE HANDLING
+// ============================================================================
+
+addEventHandler("OnPlayerChat", function(event, client, messageText) {
+ // Broadcast to all clients via custom network event
+ let chatData = {
+ text: messageText,
+ type: "normal",
+ playerName: client.name
+ };
+
+ let clients = getClients();
+ for (let i = 0; i < clients.length; i++) {
+ triggerNetworkEvent("chatMessage", clients[i], chatData);
+ }
+
+ // Log to history
+ logChat(client.name, messageText, "chat");
+
+ // Prevent default chat handling (we use custom UI)
+ return false;
+});
+
+// Network event handler for chat messages from custom UI
+addNetworkHandler("chatSendMessage", function(client, messageText) {
+ if (!messageText || messageText.length === 0) return;
+
+ // Check if it's a command
+ if (messageText.charAt(0) === '/') {
+ // Parse command
+ let parts = messageText.substring(1).split(" ");
+ let command = parts[0].toLowerCase();
+ let params = parts.slice(1).join(" ");
+
+ handleCommand(client, command, params);
+ return;
+ }
+
+ // Regular chat message - broadcast to all clients
+ let chatData = {
+ text: messageText,
+ type: "normal",
+ playerName: client.name
+ };
+
+ let clients = getClients();
+ for (let i = 0; i < clients.length; i++) {
+ triggerNetworkEvent("chatMessage", clients[i], chatData);
+ }
+
+ // Log to history
+ logChat(client.name, messageText, "chat");
+});
+
+// ============================================================================
+// COMMAND HANDLING
+// ============================================================================
+
+addEventHandler("OnPlayerCommand", function(event, client, command, params) {
+ handleCommand(client, command.toLowerCase(), params);
+});
+
+function handleCommand(client, cmd, params) {
+ switch(cmd) {
+ case "pm":
+ case "msg":
+ case "whisper":
+ case "w":
+ if (params && params.length > 0) {
+ let parts = params.split(" ");
+ let targetName = parts[0];
+ let messageText = parts.slice(1).join(" ");
+
+ if (messageText.length > 0) {
+ sendPrivateMessage(client, targetName, messageText);
+ } else {
+ sendSystemMessage(client, "[USAGE] /pm ");
+ }
+ } else {
+ sendSystemMessage(client, "[USAGE] /pm ");
+ }
+ break;
+
+ case "me":
+ case "action":
+ if (params && params.length > 0) {
+ broadcastChatMessage("* " + client.name + " " + params, "action");
+ logChat(client.name, params, "action");
+ } else {
+ sendSystemMessage(client, "[USAGE] /me ");
+ }
+ break;
+
+ case "do":
+ if (params && params.length > 0) {
+ broadcastChatMessage("* " + params + " (" + client.name + ")", "action");
+ logChat(client.name, params, "do");
+ } else {
+ sendSystemMessage(client, "[USAGE] /do ");
+ }
+ break;
+
+ case "shout":
+ case "s":
+ if (params && params.length > 0) {
+ broadcastChatMessage(client.name + " shouts: " + params.toUpperCase() + "!", "shout");
+ logChat(client.name, params, "shout");
+ } else {
+ sendSystemMessage(client, "[USAGE] /shout ");
+ }
+ break;
+
+ case "ooc":
+ case "b":
+ if (params && params.length > 0) {
+ broadcastChatMessage("(( " + client.name + ": " + params + " ))", "ooc");
+ logChat(client.name, params, "ooc");
+ } else {
+ sendSystemMessage(client, "[USAGE] /ooc ");
+ }
+ break;
+
+ case "local":
+ case "l":
+ if (params && params.length > 0) {
+ sendLocalMessage(client, params);
+ } else {
+ sendSystemMessage(client, "[USAGE] /local ");
+ }
+ break;
+
+ case "reply":
+ case "r":
+ if (params && params.length > 0) {
+ replyToLastPM(client, params);
+ } else {
+ sendSystemMessage(client, "[USAGE] /r ");
+ }
+ break;
+
+ case "clear":
+ case "cls":
+ // Clear chat for this player
+ sendSystemMessage(client, "Chat cleared");
+ break;
+ }
+}
+
+// ============================================================================
+// MESSAGE FUNCTIONS
+// ============================================================================
+
+function broadcastChatMessage(text, type, senderName) {
+ let chatData = {
+ text: text,
+ type: type || "normal",
+ playerName: senderName || null
+ };
+
+ let clients = getClients();
+ for (let i = 0; i < clients.length; i++) {
+ triggerNetworkEvent("chatMessage", clients[i], chatData);
+ }
+}
+
+function sendChatMessage(client, text, type, playerName) {
+ let chatData = {
+ text: text,
+ type: type || "normal",
+ playerName: playerName || null
+ };
+
+ triggerNetworkEvent("chatMessage", client, chatData);
+}
+
+function sendSystemMessage(client, text) {
+ sendChatMessage(client, text, "system");
+ // Also send via native for fallback
+ messageClient(text, client, COLOUR_SYSTEM);
+}
+
+function sendPrivateMessage(sender, targetName, messageText) {
+ let target = findPlayer(targetName);
+
+ if (target) {
+ if (target.index === sender.index) {
+ sendSystemMessage(sender, "[PM] You cannot message yourself!");
+ return;
+ }
+
+ // Send to target via custom UI
+ sendChatMessage(target, "[PM from " + sender.name + "]: " + messageText, "whisper");
+
+ // Confirm to sender via custom UI
+ sendChatMessage(sender, "[PM to " + target.name + "]: " + messageText, "whisper");
+
+ // Store for reply function
+ lastPMSender[target.index] = sender.index;
+
+ logChat(sender.name, "-> " + target.name + ": " + messageText, "pm");
+ } else {
+ sendSystemMessage(sender, "[PM] Player not found: " + targetName);
+ }
+}
+
+function replyToLastPM(client, messageText) {
+ if (lastPMSender[client.index] !== undefined) {
+ let targetIndex = lastPMSender[client.index];
+ let clients = getClients();
+ let target = null;
+
+ for (let i = 0; i < clients.length; i++) {
+ if (clients[i].index === targetIndex) {
+ target = clients[i];
+ break;
+ }
+ }
+
+ if (target) {
+ sendPrivateMessage(client, target.name, messageText);
+ } else {
+ sendSystemMessage(client, "[PM] The player you're trying to reply to is no longer online");
+ }
+ } else {
+ sendSystemMessage(client, "[PM] No one has messaged you yet");
+ }
+}
+
+function sendLocalMessage(sender, messageText) {
+ if (!sender.player) {
+ sendSystemMessage(sender, "[LOCAL] You need to spawn first!");
+ return;
+ }
+
+ let senderPos = sender.player.position;
+ let localRange = 30.0; // 30 units range for local chat
+ let clients = getClients();
+
+ // Send to nearby players
+ for (let i = 0; i < clients.length; i++) {
+ let client = clients[i];
+ if (client.player) {
+ let clientPos = client.player.position;
+ let distance = getDistance(senderPos, clientPos);
+
+ if (distance <= localRange) {
+ sendChatMessage(client, "(Local) " + sender.name + ": " + messageText, "local");
+ }
+ }
+ }
+
+ logChat(sender.name, messageText, "local");
+}
+
+function findPlayer(name) {
+ let clients = getClients();
+ let nameLower = name.toLowerCase();
+
+ for (let i = 0; i < clients.length; i++) {
+ if (clients[i].name.toLowerCase() === nameLower ||
+ clients[i].name.toLowerCase().indexOf(nameLower) !== -1) {
+ return clients[i];
+ }
+ }
+ return null;
+}
+
+function getDistance(pos1, pos2) {
+ let dx = pos1.x - pos2.x;
+ let dy = pos1.y - pos2.y;
+ let dz = pos1.z - pos2.z;
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+}
+
+function logChat(playerName, messageText, type) {
+ let entry = {
+ time: new Date().toISOString(),
+ player: playerName,
+ message: messageText,
+ type: type
+ };
+
+ chatHistory.push(entry);
+
+ // Keep history limited
+ if (chatHistory.length > maxHistory) {
+ chatHistory.shift();
+ }
+
+ console.log("[Chat][" + type + "] " + playerName + ": " + messageText);
+}
+
+console.log("[Chat] Server script loaded - Welcome/Leave messages enabled!");