From 6e3499812e8d0d7b9e093b9ea3cf9a1df39c167e Mon Sep 17 00:00:00 2001 From: Caesar2011 Date: Sat, 16 May 2026 01:29:21 +0200 Subject: [PATCH] Updated watch and control.lua --- control.lua | 28 ++++--------- node/watch.ts | 112 ++++++++++++++++++++++---------------------------- 2 files changed, 59 insertions(+), 81 deletions(-) diff --git a/control.lua b/control.lua index a98056e..ea3d23b 100644 --- a/control.lua +++ b/control.lua @@ -1,5 +1,5 @@ local EXPORTER_NAME = "signal-exporter" -local EXPORT_INTERVAL = 18000 -- Ticks between file writes (18000 = 5 minutes) +local EXPORT_INTERVAL = 1800 -- Ticks between file writes (1800 = 30s at 60 UPS) local function init_global() global.exporters = global.exporters or {} @@ -17,6 +17,7 @@ local function export_combinator(unit_number, data) end local signals = { + game_tick = game.tick, circuit_network = { red = {}, green = {} @@ -114,17 +115,17 @@ script.on_event(defines.events.on_gui_opened, function(event) local frame = screen.add{type = "frame", name = "signal_exporter_gui", caption = "Configure Signal Exporter", direction = "vertical"} frame.force_auto_center() - + local name_flow = frame.add{type = "flow", name = "name_flow", direction = "horizontal"} name_flow.style.vertical_align = "center" name_flow.style.bottom_margin = 8 name_flow.add{type = "label", caption = "Combinator Name: "} name_flow.add{type = "textfield", name = "exporter_name_input", text = data.name} - + local button_flow = frame.add{type = "flow", name = "button_flow", direction = "horizontal"} button_flow.add{type = "button", name = "exporter_save_button", caption = "Save & Close"} button_flow.add{type = "button", name = "exporter_refresh_button", caption = "Export Now"} - + player.opened = frame global.open_guis[player.index] = entity.unit_number end @@ -135,20 +136,20 @@ script.on_event(defines.events.on_gui_click, function(event) if element.name == "exporter_save_button" or element.name == "exporter_refresh_button" then local player = game.players[event.player_index] local unit_number = global.open_guis[player.index] - + if unit_number and global.exporters[unit_number] then local frame = player.gui.screen["signal_exporter_gui"] if frame then local new_name = frame.name_flow.exporter_name_input.text global.exporters[unit_number].name = new_name end - + if element.name == "exporter_refresh_button" then export_combinator(unit_number, global.exporters[unit_number]) player.print("Data manually exported to signals_" .. global.exporters[unit_number].name .. ".json") end end - + if element.name == "exporter_save_button" then if player.gui.screen["signal_exporter_gui"] then player.gui.screen["signal_exporter_gui"].destroy() @@ -165,41 +166,30 @@ script.on_event(defines.events.on_gui_closed, function(event) end end) - --- Function to export all item prototypes to a CSV file. local function export_item_names_csv(event) local player = game.players[event.player_index] local csv_lines = { '"item_key","localised_name"' } - -- Helper function to safely escape double quotes for CSV format. local function escape_csv_field(value) if value == nil then return "" end return string.gsub(tostring(value), '"', '""') end - -- Iterate through all registered item prototypes. for _, item_prototype in pairs(game.item_prototypes) do local key = item_prototype.name - - -- Use player.localise() to resolve the LocalisedString object (item_prototype.localised_name) - -- into the final, human-readable text for the current player's language. local resolved_name = player.localise(item_prototype.localised_name) - local escaped_key = escape_csv_field(key) local escaped_name = escape_csv_field(resolved_name) - table.insert(csv_lines, '"' .. escaped_key .. '","' .. escaped_name .. '"') end local csv_content = table.concat(csv_lines, "\n") game.write_file("item_names.csv", csv_content, false, player.index) - player.print("Export complete. File saved to 'script-output/item_names.csv'.") end --- Register a console command to trigger the export. commands.add_command( "export-item-names", "Exports the internal name (key) and localised name for all game items to a CSV file.", export_item_names_csv -) \ No newline at end of file +) diff --git a/node/watch.ts b/node/watch.ts index 18233b8..dd9d511 100644 --- a/node/watch.ts +++ b/node/watch.ts @@ -3,37 +3,33 @@ import { readdir, readFile, stat } from 'fs/promises'; import { resolve, basename, extname, join, dirname } from 'path'; import { fileURLToPath } from 'url'; -// --- Configuration --- - const __dirname = dirname(fileURLToPath(import.meta.url)); -function loadEnv() { +function loadEnv(): void { const envPath = resolve(__dirname, '.env'); try { - const envFileContent = readFileSync(envPath, 'utf-8'); - envFileContent.split(/\r?\n/).forEach(line => { - const trimmedLine = line.trim(); - if (trimmedLine && !trimmedLine.startsWith('#')) { - const parts = trimmedLine.split('=', 2); - if (parts.length === 2) { - const [key, value] = parts; - process.env[key.trim()] = value.trim().replace(/^"|"$/g, ''); - } + const content = readFileSync(envPath, 'utf-8'); + for (const line of content.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const [key, value] = trimmed.split('=', 2); + if (key && value !== undefined) { + process.env[key.trim()] = value.trim().replace(/^"|"$/g, ''); } - }); - console.log('Successfully loaded configuration from .env file.'); - } catch (error) { - console.error('FATAL: Could not find or read the .env file. Please ensure it exists in the same directory as this script.'); + } + console.log('Loaded configuration from .env'); + } catch { + console.error('FATAL: Could not read .env file.'); process.exit(1); } } loadEnv(); -const { WEBHOOK_BASE_URL, BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD } = process.env; +const { INGEST_BASE_URL, API_TOKEN } = process.env; -if (!WEBHOOK_BASE_URL || !BASIC_AUTH_USERNAME || !BASIC_AUTH_PASSWORD) { - console.error('FATAL: Please define WEBHOOK_BASE_URL, BASIC_AUTH_USERNAME, and BASIC_AUTH_PASSWORD in your .env file.'); +if (!INGEST_BASE_URL || !API_TOKEN) { + console.error('FATAL: INGEST_BASE_URL and API_TOKEN must be defined in .env'); process.exit(1); } @@ -41,100 +37,92 @@ const WATCH_PATH = resolve(__dirname, '..', '..', '..', 'script-output', 'signal const DEBOUNCE_DELAY_MS = 250; const debounceTimers = new Map(); -// --- Core Logic --- +async function sendToWebhook(filePath: string, mtimeMs: number): Promise { + const combinatorName = basename(filePath, extname(filePath)); + const url = `${INGEST_BASE_URL}/api/ingest/${encodeURIComponent(combinatorName)}?token=${API_TOKEN}`; -async function sendToWebhook(filePath: string) { try { - const combinatorName = basename(filePath, extname(filePath)); - const url = `${WEBHOOK_BASE_URL}/${encodeURIComponent(combinatorName)}`; const content = await readFile(filePath, 'utf-8'); - - // Silently ignore empty files, which can occur during file writing. if (!content.trim()) return; - JSON.parse(content); + JSON.parse(content); // validate before sending - const authHeader = 'Basic ' + Buffer.from(`${BASIC_AUTH_USERNAME}:${BASIC_AUTH_PASSWORD}`).toString('base64'); - - console.log(`Sending data for '${combinatorName}'...`); + console.log(`Sending '${combinatorName}' (mtime: ${new Date(mtimeMs).toISOString()})`); const response = await fetch(url, { - method: 'POST', // Using POST to send a request body, as is standard for webhooks. + method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': authHeader, + 'X-File-Mtime': String(mtimeMs), }, body: content, }); if (!response.ok) { - throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); + throw new Error(`HTTP ${response.status} ${response.statusText}`); } - console.log(`Successfully sent data for '${combinatorName}'. Server responded with ${response.status}.`); + console.log(`OK '${combinatorName}' → ${response.status}`); } catch (err: any) { - const fileName = basename(filePath); if (err instanceof SyntaxError) { - console.error(`Skipping ${fileName}: Invalid JSON content. The file might be incomplete.`); + console.error(`Skip '${combinatorName}': invalid JSON`); } else { - console.error(`Error sending webhook for ${fileName}:`, err.message); + console.error(`Error '${combinatorName}':`, err.message); } } } -function handleFileChange(filename: string | null) { - if (!filename || !filename.endsWith('.json')) return; +function handleFileChange(filename: string | null): void { + if (!filename?.endsWith('.json')) return; - if (debounceTimers.has(filename)) { - clearTimeout(debounceTimers.get(filename)!); - } + const existing = debounceTimers.get(filename); + if (existing) clearTimeout(existing); - const newTimer = setTimeout(async () => { + const timer = setTimeout(async () => { const fullPath = join(WATCH_PATH, filename); try { const stats = await stat(fullPath); if (stats.isFile()) { - await sendToWebhook(fullPath); + await sendToWebhook(fullPath, stats.mtimeMs); } } catch { - // Error (e.g., file deleted before processing), ignore. + // file removed before processing — ignore } finally { debounceTimers.delete(filename); } }, DEBOUNCE_DELAY_MS); - debounceTimers.set(filename, newTimer); + debounceTimers.set(filename, timer); } -async function initialScanAndProcess() { - console.log(`Performing initial scan of directory: ${WATCH_PATH}`); +async function initialScan(): Promise { + console.log(`Initial scan: ${WATCH_PATH}`); try { - const files = await readdir(WATCH_PATH); - const jsonFiles = files.filter(f => f.endsWith('.json')); - console.log(`Found ${jsonFiles.length} existing JSON file(s). Processing...`); - for (const file of jsonFiles) { - await sendToWebhook(join(WATCH_PATH, file)); + const files = (await readdir(WATCH_PATH)).filter(f => f.endsWith('.json')); + console.log(`Found ${files.length} file(s)`); + for (const file of files) { + const fullPath = join(WATCH_PATH, file); + const stats = await stat(fullPath); + await sendToWebhook(fullPath, stats.mtimeMs); } } catch (err: any) { - console.error(`Error during initial scan: ${err.message}. Ensure the directory exists.`); + console.error(`Initial scan failed: ${err.message}`); } } -// --- Script Execution --- - (async () => { - await initialScanAndProcess(); - - console.log(`\nWatching for file changes in: ${WATCH_PATH}`); + await initialScan(); + + console.log(`Watching: ${WATCH_PATH}`); try { watch(WATCH_PATH, (eventType, filename) => { if (filename) { - console.log(`[${new Date().toLocaleTimeString()}] Event '${eventType}' on '${filename}'`); + console.log(`[${new Date().toLocaleTimeString()}] ${eventType}: ${filename}`); handleFileChange(filename); } }); - console.log("Watcher is now running. Press Ctrl+C to stop."); + console.log('Watcher running. Ctrl+C to stop.'); } catch (err: any) { - console.error(`FATAL: Failed to start watcher. Please check if the path is correct. Error: ${err.message}`); - process.exit(1); + console.error(`FATAL: Watcher failed: ${err.message}`); + process.exit(1); } })();