- FriendsManager persists a per-user friend list at config/clientirc-friends.json. - MarkerManager holds one ephemeral marker per owner (5 min expiry). - IRC protocol extended with marker_set / marker_remove; server relays them. - IrcService DisplayLine replaced with sealed IrcEvent (Chat / System / MarkerSet / MarkerRemove); chat formatting moved into the client. - ClientIrcMod handles .friends add/remove/list, .marker remove, .help; chat lines from friends are prefixed with a green dot; marker events from non-friends are ignored. - OverlayHud draws a top-center compass strip and a top-left watermark (nick / FPS / ping) via HudLayerRegistrationCallback. - WorldOverlayRenderer draws green crosses above friends and magenta beams + base cubes at active markers via WorldRenderEvents.AFTER_ENTITIES. - MiddleClickHandler binds MMB via KeyMapping, raycasts a block or 20-block look ray, stores locally and sends marker_set on a daemon executor. - ChatScreenMixin completes . prefix commands on Tab; first mixin in the project, plumbed through clientirc.client.mixins.json and fabric.mod.json.
173 lines
4.7 KiB
JavaScript
173 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const net = require('node:net');
|
|
|
|
const configPath = path.join(__dirname, 'config.json');
|
|
const defaultConfig = {
|
|
host: '0.0.0.0',
|
|
port: 24469,
|
|
token: ''
|
|
};
|
|
|
|
if (!fs.existsSync(configPath)) {
|
|
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
}
|
|
|
|
const config = {
|
|
...defaultConfig,
|
|
...JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
};
|
|
|
|
const clients = new Set();
|
|
|
|
function sendJson(socket, payload) {
|
|
socket.write(JSON.stringify(payload) + '\n');
|
|
}
|
|
|
|
function broadcast(payload) {
|
|
const line = JSON.stringify(payload) + '\n';
|
|
for (const socket of clients) {
|
|
if (!socket.destroyed) {
|
|
socket.write(line);
|
|
}
|
|
}
|
|
}
|
|
|
|
function systemMessage(message) {
|
|
return {
|
|
type: 'system',
|
|
message,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
const server = net.createServer((socket) => {
|
|
socket.setEncoding('utf8');
|
|
clients.add(socket);
|
|
let buffer = '';
|
|
|
|
sendJson(socket, systemMessage('Connected to IRC server.'));
|
|
console.log(`[connect] ${socket.remoteAddress}:${socket.remotePort}`);
|
|
|
|
socket.on('data', (chunk) => {
|
|
buffer += chunk;
|
|
|
|
let separatorIndex = buffer.indexOf('\n');
|
|
while (separatorIndex !== -1) {
|
|
const rawLine = buffer.slice(0, separatorIndex).trim();
|
|
buffer = buffer.slice(separatorIndex + 1);
|
|
separatorIndex = buffer.indexOf('\n');
|
|
|
|
if (!rawLine) {
|
|
continue;
|
|
}
|
|
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(rawLine);
|
|
} catch (error) {
|
|
sendJson(socket, systemMessage(`Bad JSON: ${error.message}`));
|
|
continue;
|
|
}
|
|
|
|
if (config.token && payload.token !== config.token) {
|
|
sendJson(socket, systemMessage('Invalid token.'));
|
|
continue;
|
|
}
|
|
|
|
if (payload.type === 'hello') {
|
|
const name = payload.player || 'unknown';
|
|
sendJson(socket, systemMessage(`Hello, ${name}.`));
|
|
continue;
|
|
}
|
|
|
|
if (payload.type === 'chat') {
|
|
const clientName = String(payload.client || 'UNKNOWN-CLIENT').trim();
|
|
const playerName = String(payload.player || 'unknown').trim();
|
|
const message = String(payload.message || '').trim();
|
|
|
|
if (!message) {
|
|
sendJson(socket, systemMessage('Empty chat message ignored.'));
|
|
continue;
|
|
}
|
|
|
|
const outgoing = {
|
|
type: 'chat',
|
|
client: clientName,
|
|
player: playerName,
|
|
message,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
console.log(`[chat] ${clientName} >> ${playerName}: ${message}`);
|
|
broadcast(outgoing);
|
|
continue;
|
|
}
|
|
|
|
if (payload.type === 'marker_set') {
|
|
const clientName = String(payload.client || 'UNKNOWN-CLIENT').trim();
|
|
const playerName = String(payload.player || 'unknown').trim();
|
|
const dimensionId = String(payload.dimensionId || 'minecraft:overworld').trim();
|
|
const x = Number(payload.x);
|
|
const y = Number(payload.y);
|
|
const z = Number(payload.z);
|
|
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
sendJson(socket, systemMessage('Bad marker_set coords.'));
|
|
continue;
|
|
}
|
|
const outgoing = {
|
|
type: 'marker_set',
|
|
client: clientName,
|
|
player: playerName,
|
|
dimensionId,
|
|
x,
|
|
y,
|
|
z,
|
|
timestamp: Date.now()
|
|
};
|
|
console.log(`[marker_set] ${clientName} >> ${playerName} @ ${dimensionId} ${x.toFixed(1)},${y.toFixed(1)},${z.toFixed(1)}`);
|
|
broadcast(outgoing);
|
|
continue;
|
|
}
|
|
|
|
if (payload.type === 'marker_remove') {
|
|
const clientName = String(payload.client || 'UNKNOWN-CLIENT').trim();
|
|
const playerName = String(payload.player || 'unknown').trim();
|
|
const outgoing = {
|
|
type: 'marker_remove',
|
|
client: clientName,
|
|
player: playerName,
|
|
timestamp: Date.now()
|
|
};
|
|
console.log(`[marker_remove] ${clientName} >> ${playerName}`);
|
|
broadcast(outgoing);
|
|
continue;
|
|
}
|
|
|
|
sendJson(socket, systemMessage(`Unknown packet type: ${payload.type}`));
|
|
}
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
clients.delete(socket);
|
|
console.log(`[disconnect] ${socket.remoteAddress}:${socket.remotePort}`);
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
|
clients.delete(socket);
|
|
console.error(`[socket-error] ${socket.remoteAddress}:${socket.remotePort} ${error.message}`);
|
|
});
|
|
});
|
|
|
|
server.on('error', (error) => {
|
|
console.error(`[server-error] ${error.message}`);
|
|
process.exitCode = 1;
|
|
});
|
|
|
|
server.listen(config.port, config.host, () => {
|
|
console.log(`IRC server listening on ${config.host}:${config.port}`);
|
|
});
|
|
|