From e72b849774e0b9e8b638b0ef19bf967df444d46b Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 25 Aug 2026 12:29:07 -0400 Subject: [PATCH] feat(scripts): tee bin script logs to daily files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add createBinLogger (src/scripts/lib/binFileLogger.ts): every log call is appended synchronously to logs//.log (UTC) in addition to the console logger — sync writes because bin scripts process.exit() immediately. Bin bodies are wrapped in try/catch so a fatal error is logged to file and exits code 1. Override the directory with BIN_LOG_DIR. logs/ is gitignored. game-tick, market-tick, and processShipmentTick adopt the logger; generate-mission follows with its clone fix; mission-tick lands with its feature. Document BIN_LOG_DIR in .env.example and the logging in AGENTS.md / README. --- .env.example | 7 + .gitignore | 3 + AGENTS.md | 1 + README.md | 3 + src/scripts/gameTick.ts | 120 ++++++------ src/scripts/lib/binFileLogger.ts | 77 ++++++++ src/scripts/marketTick.ts | 302 +++++++++++++++-------------- src/scripts/processShipmentTick.ts | 14 +- 8 files changed, 317 insertions(+), 210 deletions(-) create mode 100644 src/scripts/lib/binFileLogger.ts diff --git a/.env.example b/.env.example index 67488d0..e9e6758 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,13 @@ APP_URL=http://localhost:3000 # Coolify (or to your cron). See DEPLOYMENT.md. GAME_TICK_NOTIFY_SECRET=YOUR_GAME_TICK_NOTIFY_SECRET +# --- Bin script file logging --- +# Optional. Payload bin scripts (game-tick, market-tick, mission-tick, +# generate-mission) tee their logs to daily files. Defaults to /logs +# (inside containers: /app/logs — mount a persistent volume or point this at +# a persisted path, e.g. /app/media/logs, to keep them across restarts). +# BIN_LOG_DIR= + # --- Payload email (nodemailer / SMTP relay) --- # Optional. Leave blank to disable outbound email (password resets, etc). EMAIL_FROM_ADDRESS= diff --git a/.gitignore b/.gitignore index e87e3e0..a58e438 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# bin script file logs (game-tick, market-tick, mission-tick, generate-mission) +/logs/ + # local env files .env*.local diff --git a/AGENTS.md b/AGENTS.md index 1ac2c4f..6d7957d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,6 +147,7 @@ Shipping simulation (`src/collections/logistics/Shipments.ts`, `src/scripts/`, ` - **Shipment fields**: `origin`/`destination` → `game-structures`, `transportVehicle` → `game-vehicles`, `cargo[]` (relationship to resources/assets/vehicles + amount), `distance`, `fuelCost`/`fuelConsumed`, `status` (`pending`/`dispatched`/`in_transit`/`arrived`/`completed`/`cancelled`/`failed`/`stranded`), `autoReturn` checkbox, `failureReason`. - **Game tick**: `bun run payload game-tick` — a `bin` registered on `payload.config.ts`, NOT an npm script. It processes active shipments + fuel consumption, then `process.exit(0)`. +- **Bin file logging**: all four bins (`game-tick`, `market-tick`, `mission-tick`, `generate-mission`) tee their logs to daily files `logs//.log` (UTC) via `createBinLogger` in `src/scripts/lib/binFileLogger.ts` — in addition to the console. Writes are `appendFileSync` (bin scripts `process.exit` immediately, so async streams would truncate). A fatal error in a bin is caught, logged to the file, and exits code 1. Directory override: `BIN_LOG_DIR` env (defaults to `/logs` — inside Docker point it at a persistent volume). `logs/` is gitignored. - **Arrival handling** (`src/scripts/processShipmentTick.ts`): destination storage rules are re-checked on arrival; rejected cargo bounces back to origin, shipment goes `failed`, `ShipmentFail` event logged. - **GameRules tuning**: `proximityThreshold` and `gameTickIntervalMinutes` live on the global `game-rules` doc. - **UI**: `src/app/(frontend)/logistics/shipments/` (list + `[id]` detail with `ShipmentActions` controls), `src/app/(frontend)/logistics/game-vehicles/` (deployed vehicle views). diff --git a/README.md b/README.md index 1cd2706..187e141 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,13 @@ Dev credentials: `dev` / `Test123` (if seed data has been applied). | `bun run payload ` | Run Payload binaries | | `bun run payload game-tick` | Process shipment arrivals and fuel consumption | | `bun run payload market-tick` | Expire listings and refresh NPC vendor stock | +| `bun run payload mission-tick` | Auto-complete missions whose scheduled day has passed | | `bun run deploy` | Bump patch version, build, and deploy | Note: `game-tick` and `market-tick` are Payload bins registered in `payload.config.ts`, not npm scripts. Run via `bun run payload`. +All four bin scripts also log to daily files under `logs//` (e.g. `logs/game-tick/2026-08-24.log`) so tick output can be tracked over time. Override the directory with the `BIN_LOG_DIR` env var. + For an Arma 3 unit deployment we self-host on **Coolify** with one container per environment (dev / stg / prod), a Coolify-managed PostgreSQL service per environment, a persistent volume for `/app/media`, and `docker exec` scheduled jobs for the game tick. The Dockerfile is Bun-based (multi-stage, Next.js standalone runtime + Payload CLI kept at runtime), and `docker-compose.yml` mirrors the same shape for local full-stack dev. For the full setup — per-env Postgres provisioning, env vars, scheduled jobs, persistent media volume, SSE single-instance caveat, rollbacks, troubleshooting — see **[DEPLOYMENT.md](./DEPLOYMENT.md)**. diff --git a/src/scripts/gameTick.ts b/src/scripts/gameTick.ts index a84d449..dde266d 100644 --- a/src/scripts/gameTick.ts +++ b/src/scripts/gameTick.ts @@ -2,70 +2,76 @@ import type { SanitizedConfig } from "payload"; import { getPayload } from "payload"; import type { GameStructure, GameVehicle, Shipment, Vehicle } from "@/payload-types"; import { processShipmentTick } from "./processShipmentTick"; +import { createBinLogger, errorMessage } from "./lib/binFileLogger"; import { emitGameEvent } from "@/utils/event-log/emit"; import { EventTypes } from "@/utils/event-log/eventTypes"; export const script = async (config: SanitizedConfig) => { const payload = await getPayload({ config }); + const log = createBinLogger("game-tick", payload.logger); - payload.logger.info("[Game Tick] Starting game tick..."); + try { + log.info("[Game Tick] Starting game tick..."); - const activeShipments = await payload.find({ - collection: "shipments", - where: { - status: { in: ["dispatched", "in_transit"] }, - }, - limit: 100, - depth: 2, - }); - - let processedCount = 0; - for (const shipment of activeShipments.docs) { - try { - await processShipmentTick( - payload, - shipment as unknown as Shipment & { - transportVehicle: GameVehicle & { type: Vehicle }; - origin: GameStructure; - destination: GameStructure; - }, - ); - processedCount++; - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - payload.logger.error(`[Game Tick] Error processing shipment ${shipment.id}: ${message}`); - } - } - - await emitGameEvent(payload, { - type: EventTypes.GameTick, - message: `Game tick processed ${processedCount} of ${activeShipments.docs.length} active shipments`, - data: { - processedCount, - totalActive: activeShipments.docs.length, - }, - }); - - const notifyUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, ""); - const notifySecret = process.env.GAME_TICK_NOTIFY_SECRET; - if (notifySecret) { - await fetch(`${notifyUrl}/api/game-tick/notify`, { - method: "POST", - headers: { - "content-type": "application/json", - "x-game-tick-secret": notifySecret, + const activeShipments = await payload.find({ + collection: "shipments", + where: { + status: { in: ["dispatched", "in_transit"] }, }, - body: JSON.stringify({ processedAt: new Date().toISOString() }), - }).catch((error: unknown) => { - const message = error instanceof Error ? error.message : "Unknown error"; - payload.logger.error(`[Game Tick] Failed to notify app clients: ${message}`); + limit: 100, + depth: 2, }); - } else { - payload.logger.warn( - "[Game Tick] GAME_TICK_NOTIFY_SECRET not set; skipping client notification", - ); - } - payload.logger.info(`[Game Tick] Complete. Processed ${processedCount} shipments.`); - process.exit(0); -}; \ No newline at end of file + let processedCount = 0; + for (const shipment of activeShipments.docs) { + try { + await processShipmentTick( + payload, + shipment as unknown as Shipment & { + transportVehicle: GameVehicle & { type: Vehicle }; + origin: GameStructure; + destination: GameStructure; + }, + log, + ); + processedCount++; + } catch (error) { + log.error(`[Game Tick] Error processing shipment ${shipment.id}: ${errorMessage(error)}`); + } + } + + await emitGameEvent(payload, { + type: EventTypes.GameTick, + message: `Game tick processed ${processedCount} of ${activeShipments.docs.length} active shipments`, + data: { + processedCount, + totalActive: activeShipments.docs.length, + }, + }); + + const notifyUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, ""); + const notifySecret = process.env.GAME_TICK_NOTIFY_SECRET; + if (notifySecret) { + await fetch(`${notifyUrl}/api/game-tick/notify`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-game-tick-secret": notifySecret, + }, + body: JSON.stringify({ processedAt: new Date().toISOString() }), + }).catch((error: unknown) => { + log.error(`[Game Tick] Failed to notify app clients: ${errorMessage(error)}`); + }); + } else { + log.warn("[Game Tick] GAME_TICK_NOTIFY_SECRET not set; skipping client notification"); + } + + log.info(`[Game Tick] Complete. Processed ${processedCount} shipments.`); + process.exit(0); + } catch (error) { + log.error( + `[Game Tick] Fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`, + ); + process.exit(1); + } +}; diff --git a/src/scripts/lib/binFileLogger.ts b/src/scripts/lib/binFileLogger.ts new file mode 100644 index 0000000..5272898 --- /dev/null +++ b/src/scripts/lib/binFileLogger.ts @@ -0,0 +1,77 @@ +import fs from "node:fs"; +import path from "node:path"; + +/** + * File logging for Payload bin scripts (game-tick, market-tick, mission-tick, + * generate-mission). + * + * Each log call is appended SYNCHRONOUSLY to a daily file under + * `logs//.log` (UTC) and also forwarded to the console + * logger (payload.logger), so console output is unchanged. Sync writes are + * deliberate: bin scripts call `process.exit(0)` immediately after their last + * log line, which would truncate anything still buffered in an async stream + * (Payload uses a sync logger for bins for the same reason). + * + * File logging must never break the tick: all fs errors are swallowed and the + * message still goes to console. + * + * Override the directory with `BIN_LOG_DIR` (e.g. point it at a persistent + * volume in container deployments — by default it is `/logs`, which is + * ephemeral inside Docker). + */ + +/** Minimal structural type satisfied by `payload.logger` (pino). */ +export interface ConsoleLogger { + info: (msg: string) => void; + warn: (msg: string) => void; + error: (msg: string) => void; +} + +export interface BinLogger { + info: (msg: string) => void; + warn: (msg: string) => void; + error: (msg: string) => void; +} + +type Level = "INFO" | "WARN" | "ERROR"; + +function resolveLogDir(scriptKey: string): string { + const base = process.env.BIN_LOG_DIR ?? path.join(process.cwd(), "logs"); + return path.join(base, scriptKey); +} + +function logFilePath(logDir: string, now: Date): string { + return path.join(logDir, `${now.toISOString().slice(0, 10)}.log`); +} + +function writeLogLine(logDir: string, level: Level, msg: string): void { + try { + const now = new Date(); + fs.mkdirSync(logDir, { recursive: true }); + fs.appendFileSync(logFilePath(logDir, now), `${now.toISOString()} [${level}] ${msg}\n`); + } catch { + // File logging is best-effort; the console copy below still goes out. + } +} + +export function createBinLogger(scriptKey: string, consoleLogger: ConsoleLogger): BinLogger { + const logDir = resolveLogDir(scriptKey); + + const tee = + (level: Level, consoleFn: (msg: string) => void): ((msg: string) => void) => + (msg: string) => { + writeLogLine(logDir, level, msg); + consoleFn(msg); + }; + + return { + info: tee("INFO", (msg) => consoleLogger.info(msg)), + warn: tee("WARN", (msg) => consoleLogger.warn(msg)), + error: tee("ERROR", (msg) => consoleLogger.error(msg)), + }; +} + +/** Uniform message extraction for caught unknowns (matches the actions.ts convention). */ +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : "Unknown error"; +} diff --git a/src/scripts/marketTick.ts b/src/scripts/marketTick.ts index 6f45d71..4ce8305 100644 --- a/src/scripts/marketTick.ts +++ b/src/scripts/marketTick.ts @@ -8,162 +8,166 @@ import { applyNpcPriceModifier, resolveVendorNpc } from "@/lib/market/npcs"; import { expireNegotiationsForListing } from "@/lib/market/negotiations"; import { resolveAssetId } from "@/lib/locker"; import { getMainCurrencyId } from "@/lib/banking"; +import { createBinLogger, errorMessage } from "./lib/binFileLogger"; export const script = async (config: SanitizedConfig) => { const payload = await getPayload({ config }); + const log = createBinLogger("market-tick", payload.logger); - payload.logger.info("[Market Tick] Starting market tick..."); + try { + log.info("[Market Tick] Starting market tick..."); - const now = new Date(); - const nowIso = now.toISOString(); + const now = new Date(); + const nowIso = now.toISOString(); - // 1. Expire overdue listings (returns stock to sellers for user listings). - const expiring = await payload.find({ - collection: "market-listings", - where: { - and: [{ status: { equals: "active" } }, { expiresAt: { less_than_equal: nowIso } }], - }, - limit: 200, - depth: 1, - sort: "expiresAt", - }); - - let expiredCount = 0; - let expiredUserCount = 0; - for (const listing of expiring.docs) { - const sellerId = - typeof listing.seller === "object" && listing.seller - ? (listing.seller as { id: number }).id - : (listing.seller as number | null); - const isUserListing = sellerId != null && !listing.isAutoGenerated; - try { - await expireListing(payload, listing as unknown as MarketListing); - expiredCount++; - if (isUserListing) expiredUserCount++; - const assetName = - typeof listing.asset === "object" && listing.asset - ? listing.asset.name - : `asset #${resolveAssetId(listing.asset)}`; - await expireNegotiationsForListing(payload, listing.id, assetName, { - reason: "expired before you could close the deal", - }); - await emitGameEvent(payload, { - type: EventTypes.MarketListingExpire, - message: `Listing for ${listing.quantity}× ${assetName} expired${ - isUserListing ? " — stock returned to seller" : "" - }`, - actor: sellerId ?? undefined, - targetCollection: "market-listings", - targetId: listing.id, - data: { - assetId: listing.asset, - assetName, - quantity: listing.quantity, - autoGenerated: listing.isAutoGenerated, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - payload.logger.error(`[Market Tick] Error expiring listing ${listing.id}: ${message}`); - } - } - - // 2. Top up auto-generated entries for tradeable live assets with no active listing. - const active = await payload.find({ - collection: "market-listings", - where: { status: { equals: "active" } }, - limit: 2000, - depth: 0, - }); - const activeAssetIds = new Set(active.docs.map((d) => resolveAssetId(d.asset))); - - const assets = await payload.find({ - collection: "assets", - where: { - and: [{ tradeable: { equals: true } }, { isLive: { equals: true } }], - }, - limit: 2000, - depth: 0, - sort: "name", - }); - - const currencyId = await getMainCurrencyId(payload); - let generatedCount = 0; - let generatedNpcCount = 0; - for (const asset of assets.docs as unknown as Asset[]) { - if (activeAssetIds.has(asset.id)) continue; - const basePrice = autoPrice(asset, "buy"); - if (basePrice == null) continue; - - // Assign an NPC to stock this entry: prefer GM-assigned vendor NPCs, - // fall back to generating a placeholder NPC when none sell this good. - const npc = await resolveVendorNpc(payload, asset.id); - if (npc.isGenerated === true) generatedNpcCount++; - const price = applyNpcPriceModifier(basePrice, npc); - if (price <= 0) continue; - - const quantity = autoQuantity(asset); - const expiresAt = new Date(Date.now() + AUTO_LISTING_DURATION_MS).toISOString(); - const minPrice = Math.max(1, Math.round(price * 0.6)); - try { - await payload.create({ - collection: "market-listings", - data: { - asset: asset.id, - seller: null, - npc: npc.id, - quantity, - price, - desiredPrice: price, - minPrice, - currency: currencyId ?? null, - status: "active", - isAutoGenerated: true, - listedAt: nowIso, - expiresAt, - }, - overrideAccess: true, - depth: 0, - }); - generatedCount++; - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - payload.logger.error( - `[Market Tick] Error generating listing for asset ${asset.id}: ${message}`, - ); - } - } - - await emitGameEvent(payload, { - type: EventTypes.MarketAutoGenerate, - message: `Market tick: expired ${expiredCount} listing(s), generated ${generatedCount} new market entry/entries${generatedNpcCount > 0 ? ` (${generatedNpcCount} new NPC${generatedNpcCount !== 1 ? "s" : ""} generated)` : ""}`, - data: { - expiredCount, - generatedCount, - generatedNpcCount, - activeListings: active.docs.length, - }, - }); - - // 3. Notify connected clients so the market refreshes. - const notifyUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, ""); - const notifySecret = process.env.GAME_TICK_NOTIFY_SECRET; - if (notifySecret) { - await fetch(`${notifyUrl}/api/game-tick/notify`, { - method: "POST", - headers: { - "content-type": "application/json", - "x-game-tick-secret": notifySecret, + // 1. Expire overdue listings (returns stock to sellers for user listings). + const expiring = await payload.find({ + collection: "market-listings", + where: { + and: [{ status: { equals: "active" } }, { expiresAt: { less_than_equal: nowIso } }], }, - body: JSON.stringify({ processedAt: nowIso, source: "market-tick" }), - }).catch((error: unknown) => { - const message = error instanceof Error ? error.message : "Unknown error"; - payload.logger.error(`[Market Tick] Failed to notify app clients: ${message}`); + limit: 200, + depth: 1, + sort: "expiresAt", }); - } - payload.logger.info( - `[Market Tick] Complete. Expired ${expiredCount}, generated ${generatedCount}.`, - ); - process.exit(0); + let expiredCount = 0; + let expiredUserCount = 0; + for (const listing of expiring.docs) { + const sellerId = + typeof listing.seller === "object" && listing.seller + ? (listing.seller as { id: number }).id + : (listing.seller as number | null); + const isUserListing = sellerId != null && !listing.isAutoGenerated; + try { + await expireListing(payload, listing as unknown as MarketListing); + expiredCount++; + if (isUserListing) expiredUserCount++; + const assetName = + typeof listing.asset === "object" && listing.asset + ? listing.asset.name + : `asset #${resolveAssetId(listing.asset)}`; + await expireNegotiationsForListing(payload, listing.id, assetName, { + reason: "expired before you could close the deal", + }); + await emitGameEvent(payload, { + type: EventTypes.MarketListingExpire, + message: `Listing for ${listing.quantity}× ${assetName} expired${ + isUserListing ? " — stock returned to seller" : "" + }`, + actor: sellerId ?? undefined, + targetCollection: "market-listings", + targetId: listing.id, + data: { + assetId: listing.asset, + assetName, + quantity: listing.quantity, + autoGenerated: listing.isAutoGenerated, + }, + }); + } catch (error) { + log.error(`[Market Tick] Error expiring listing ${listing.id}: ${errorMessage(error)}`); + } + } + + // 2. Top up auto-generated entries for tradeable live assets with no active listing. + const active = await payload.find({ + collection: "market-listings", + where: { status: { equals: "active" } }, + limit: 2000, + depth: 0, + }); + const activeAssetIds = new Set(active.docs.map((d) => resolveAssetId(d.asset))); + + const assets = await payload.find({ + collection: "assets", + where: { + and: [{ tradeable: { equals: true } }, { isLive: { equals: true } }], + }, + limit: 2000, + depth: 0, + sort: "name", + }); + + const currencyId = await getMainCurrencyId(payload); + let generatedCount = 0; + let generatedNpcCount = 0; + for (const asset of assets.docs as unknown as Asset[]) { + if (activeAssetIds.has(asset.id)) continue; + const basePrice = autoPrice(asset, "buy"); + if (basePrice == null) continue; + + // Assign an NPC to stock this entry: prefer GM-assigned vendor NPCs, + // fall back to generating a placeholder NPC when none sell this good. + const npc = await resolveVendorNpc(payload, asset.id); + if (npc.isGenerated === true) generatedNpcCount++; + const price = applyNpcPriceModifier(basePrice, npc); + if (price <= 0) continue; + + const quantity = autoQuantity(asset); + const expiresAt = new Date(Date.now() + AUTO_LISTING_DURATION_MS).toISOString(); + const minPrice = Math.max(1, Math.round(price * 0.6)); + try { + await payload.create({ + collection: "market-listings", + data: { + asset: asset.id, + seller: null, + npc: npc.id, + quantity, + price, + desiredPrice: price, + minPrice, + currency: currencyId ?? null, + status: "active", + isAutoGenerated: true, + listedAt: nowIso, + expiresAt, + }, + overrideAccess: true, + depth: 0, + }); + generatedCount++; + } catch (error) { + log.error( + `[Market Tick] Error generating listing for asset ${asset.id}: ${errorMessage(error)}`, + ); + } + } + + await emitGameEvent(payload, { + type: EventTypes.MarketAutoGenerate, + message: `Market tick: expired ${expiredCount} listing(s), generated ${generatedCount} new market entry/entries${generatedNpcCount > 0 ? ` (${generatedNpcCount} new NPC${generatedNpcCount !== 1 ? "s" : ""} generated)` : ""}`, + data: { + expiredCount, + generatedCount, + generatedNpcCount, + activeListings: active.docs.length, + }, + }); + + // 3. Notify connected clients so the market refreshes. + const notifyUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, ""); + const notifySecret = process.env.GAME_TICK_NOTIFY_SECRET; + if (notifySecret) { + await fetch(`${notifyUrl}/api/game-tick/notify`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-game-tick-secret": notifySecret, + }, + body: JSON.stringify({ processedAt: nowIso, source: "market-tick" }), + }).catch((error: unknown) => { + log.error(`[Market Tick] Failed to notify app clients: ${errorMessage(error)}`); + }); + } + + log.info(`[Market Tick] Complete. Expired ${expiredCount}, generated ${generatedCount}.`); + process.exit(0); + } catch (error) { + log.error( + `[Market Tick] Fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`, + ); + process.exit(1); + } }; diff --git a/src/scripts/processShipmentTick.ts b/src/scripts/processShipmentTick.ts index 28d02fa..b55d8a0 100644 --- a/src/scripts/processShipmentTick.ts +++ b/src/scripts/processShipmentTick.ts @@ -4,6 +4,7 @@ import { emitGameEvent } from "@/utils/event-log/emit"; import { EventTypes } from "@/utils/event-log/eventTypes"; import { notifyUser } from "@/lib/notifications"; import { checkStorageDeposit, storageViolationMessage } from "@/lib/storageRules"; +import { errorMessage, type BinLogger } from "./lib/binFileLogger"; type ShipmentDoc = Shipment & { transportVehicle: GameVehicle & { type: Vehicle }; @@ -11,7 +12,11 @@ type ShipmentDoc = Shipment & { destination: GameStructure; }; -export async function processShipmentTick(payload: Payload, shipment: ShipmentDoc): Promise { +export async function processShipmentTick( + payload: Payload, + shipment: ShipmentDoc, + log: Pick, +): Promise { const now = new Date(); const vehicle = shipment.transportVehicle; @@ -70,7 +75,7 @@ export async function processShipmentTick(payload: Payload, shipment: ShipmentDo const newFuelConsumed = Math.min(currentFuelConsumed + fuelPerTick, shipment.fuelCost); if (now >= estimatedArrival) { - await completeShipment(payload, shipment, newFuelConsumed); + await completeShipment(payload, shipment, newFuelConsumed, log); return; } @@ -88,6 +93,7 @@ async function completeShipment( payload: Payload, shipment: ShipmentDoc, finalFuelConsumed: number, + log: Pick, ): Promise { const vehicle = shipment.transportVehicle; const destinationId = @@ -138,7 +144,7 @@ async function completeShipment( amount: cargoEntry.amount, resourceName: resName, }); - console.error( + log.error( `Shipment "${shipment.name ?? `SHP-${shipment.id}`}" delivery rejected at destination: ${storageViolationMessage(resName, ruleCheck)}`, ); continue; @@ -169,7 +175,7 @@ async function completeShipment( data: { storedResources: existingResources }, }); } catch (error) { - console.error(`Failed to transfer cargo item ${itemId}:`, error); + log.error(`Failed to transfer cargo item ${itemId}: ${errorMessage(error)}`); } }