diff --git a/parsing/game-docs.json b/parsing/game-docs.json index a6ea794e..488a2397 100644 Binary files a/parsing/game-docs.json and b/parsing/game-docs.json differ diff --git a/parsing/gameData.json b/parsing/gameData.json index 2d860a7d..3f4b25d6 100644 --- a/parsing/gameData.json +++ b/parsing/gameData.json @@ -9784,7 +9784,7 @@ "isFicsmas": false }, { - "id": "CartridgeChaos_Packaged", + "id": "CartridgeChaos", "displayName": "Turbo Rifle Ammo", "ingredients": [ { @@ -9798,7 +9798,7 @@ "perMin": 15 }, { - "part": "PackagedTurboFuel", + "part": "LiquidTurboFuel", "amount": 3, "perMin": 15 } @@ -9812,14 +9812,14 @@ } ], "building": { - "name": "manufacturermk1", - "power": 55 + "name": "blender", + "power": 75 }, "isAlternate": false, "isFicsmas": false }, { - "id": "CartridgeChaos", + "id": "CartridgeChaos_Packaged", "displayName": "Turbo Rifle Ammo", "ingredients": [ { @@ -9833,7 +9833,7 @@ "perMin": 15 }, { - "part": "LiquidTurboFuel", + "part": "PackagedTurboFuel", "amount": 3, "perMin": 15 } @@ -9847,8 +9847,8 @@ } ], "building": { - "name": "blender", - "power": 75 + "name": "manufacturermk1", + "power": 55 }, "isAlternate": false, "isFicsmas": false diff --git a/parsing/src/buildings.ts b/parsing/src/buildings.ts index 762ef9b0..bf259e79 100644 --- a/parsing/src/buildings.ts +++ b/parsing/src/buildings.ts @@ -2,6 +2,15 @@ // Function to extract all buildings that produce something import {getPowerProducerBuildingName} from "./common"; +// Buildings that appear in mProducedIn but are not actual production buildings +const nonProductionBuildings = new Set([ + 'bp_buildgun', + 'bp_workbenchcomponent', + 'bp_workshopcomponent', + 'automatedworkbench', + 'factorygame', +]); + function getProducingBuildings(data: any[]): string[] { const producingBuildingsSet = new Set(); @@ -23,7 +32,12 @@ function getProducingBuildings(data: any[]): string[] { .filter((buildingName: string | null) => buildingName !== null); // Filter out null values if (producedInBuildings) { - producedInBuildings.forEach((buildingName: string) => producingBuildingsSet.add(buildingName)); + // The 1.2 game data references non-production buildings (e.g. BuildGun, + // WorkBench) in mProducedIn. Exclude them so they don't inflate the + // building map or pull in equipment/tool recipes downstream. + producedInBuildings + .filter((buildingName: string) => !nonProductionBuildings.has(buildingName)) + .forEach((buildingName: string) => producingBuildingsSet.add(buildingName)); } } // If a power generator @@ -60,6 +74,20 @@ function getPowerConsumptionForBuildings(data: any[], producingBuildings: string } }); + // Variable power buildings (e.g. Hadron Collider, Converter, Quantum Encoder) may + // have mPowerConsumption of 0 or lack it entirely. Use a sentinel value (0.1) so + // downstream recipe filtering still includes them. The actual power is calculated + // per-recipe from mVariablePowerConsumption fields. Power generators (which also + // have 0 power) are excluded — they legitimately produce power rather than consume it. + const generatorPrefix = 'generator'; + producingBuildings.forEach((buildingName: string) => { + if (!Object.prototype.hasOwnProperty.call(buildingsPowerMap, buildingName)) { + buildingsPowerMap[buildingName] = buildingName.startsWith(generatorPrefix) ? 0 : 0.1; + } else if (buildingsPowerMap[buildingName] === 0 && !buildingName.startsWith(generatorPrefix)) { + buildingsPowerMap[buildingName] = 0.1; + } + }); + // Finally sort the map by key const sortedMap: { [key: string]: number } = {}; Object.keys(buildingsPowerMap).sort().forEach(key => { diff --git a/parsing/src/parts.ts b/parsing/src/parts.ts index 60542ab4..c34cad8d 100644 --- a/parsing/src/parts.ts +++ b/parsing/src/parts.ts @@ -355,6 +355,7 @@ function fixItemNames(items: ParserItemDataInterface): void { // Go through the item names and do some manual fixes, e.g. renaming "Residual Plastic" to "Plastic" const fixItems: Record = { "AlienProtein": "Alien Protein", + "AluminumIngot": "Aluminum Ingot", // the parser uses the recipe display name instead of the part descriptor name. The "Alternate: Pure Aluminum Ingot" recipe produces AluminumIngot, so the part gets that recipe name instead of "Aluminum Ingot" "CompactedCoal": "Compacted Coal", "DarkEnergy": "Dark Matter Residue", "HeavyOilResidue": "Heavy Oil Residue", @@ -362,6 +363,7 @@ function fixItemNames(items: ParserItemDataInterface): void { "Plastic": "Plastic", "PolymerResin": "Polymer Resin", "Rubber": "Rubber", + "Silica": "Silica", // the parser uses the recipe display name instead of the part descriptor name. The "Alumina Solution" recipe produces both AluminaSolution and Silica as products — so when Silica was processed as a product of that recipe, it inherited the recipe name "Alumina Solution" instead of its own name "Silica" "Snow": "Snow", "Water": "Water", }; diff --git a/parsing/src/recipes.ts b/parsing/src/recipes.ts index 1d7b266d..07877cd1 100644 --- a/parsing/src/recipes.ts +++ b/parsing/src/recipes.ts @@ -42,7 +42,7 @@ function getProductionRecipes( // Process all buildings and check if any match the producingBuildings map const validBuilding = rawBuildingKeys.some((rawBuilding: string) => { const buildingKey: string = rawBuilding.replace(/\//g, '').replace(/\./g, '').toLowerCase().replace('build_', ''); - return producingBuildings[buildingKey]; + return typeof producingBuildings[buildingKey] === 'number'; }) return validBuilding; @@ -112,7 +112,7 @@ function getProductionRecipes( if (validBuildings.length > 0) { // Sum up power for all valid buildings powerPerBuilding = validBuildings.reduce((totalPower: number, building: string | number) => { - if (producingBuildings[building]) { + if (typeof producingBuildings[building] === 'number') { const buildingPower: number = producingBuildings[building] selectedBuilding = selectedBuilding || building; // Set the first valid building as selected return totalPower + buildingPower; // Add power for this building @@ -216,6 +216,10 @@ function getPowerGeneratingRecipes( fuels.forEach((fuel: any) => { const primaryFuel = getPartName(fuel.mFuelClass); const primaryFuelPart = parts.parts[primaryFuel]; + if (!primaryFuelPart) { + console.warn(`Skipping power recipe fuel with missing part data: ${primaryFuel}`); + return; + } const burnDurationInMins = primaryFuelPart.energyGeneratedInMJ / burnRateMJ; const burnDurationInS = burnDurationInMins * 60; // Convert to seconds diff --git a/parsing/tests/power.spec.ts b/parsing/tests/power.spec.ts index fdc7d5c3..2f80a293 100644 --- a/parsing/tests/power.spec.ts +++ b/parsing/tests/power.spec.ts @@ -1,7 +1,9 @@ import { beforeAll, describe, expect, it } from '@jest/globals' import { processFile } from '../src/processor' +import { getPowerGeneratingRecipes } from '../src/recipes' import {ParserPowerRecipe} from "../src/interfaces/ParserPowerRecipe"; +import {ParserItemDataInterface} from "../src/interfaces/ParserPart"; describe('Power Parsing', () => { let results: any; @@ -145,4 +147,66 @@ describe('Power Parsing', () => { expect(recipe.building.power).toBe(2500); }) }) + + describe('Missing fuel part handling', () => { + it('should skip a fuel whose part is missing from items.parts and warn', () => { + const fakeData = [ + { + Classes: [ + { + ClassName: "Build_GeneratorFake_C", + mPowerProduction: "100.000000", + mSupplementalToPowerRatio: "0", + mFuel: [ + { + mFuelClass: "Desc_KnownFuel_C", + mSupplementalResourceClass: "", + mByproduct: "", + mByproductAmount: "", + }, + { + mFuelClass: "Desc_MissingFuel_C", + mSupplementalResourceClass: "", + mByproduct: "", + mByproductAmount: "", + }, + ], + }, + ], + }, + ]; + + const items: ParserItemDataInterface = { + parts: { + KnownFuel: { + name: "Known Fuel", + stackSize: 100, + isFluid: false, + isFicsmas: false, + energyGeneratedInMJ: 600, + }, + // MissingFuel is deliberately absent + }, + rawResources: {}, + }; + + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + let recipes: ParserPowerRecipe[]; + expect(() => { + recipes = getPowerGeneratingRecipes(fakeData, items); + }).not.toThrow(); + + // The known fuel should produce a recipe; the missing one should be skipped + expect(recipes!.length).toBe(1); + expect(recipes![0].ingredients[0].part).toBe('KnownFuel'); + + // Verify the warning was emitted for the missing part + expect(warnSpy).toHaveBeenCalledWith( + 'Skipping power recipe fuel with missing part data: MissingFuel' + ); + + warnSpy.mockRestore(); + }) + }) }) diff --git a/web/public/gameData_v1.0-29.json b/web/public/gameData_v1.2-00.json similarity index 100% rename from web/public/gameData_v1.0-29.json rename to web/public/gameData_v1.2-00.json index 2d860a7d..3f4b25d6 100644 --- a/web/public/gameData_v1.0-29.json +++ b/web/public/gameData_v1.2-00.json @@ -9784,7 +9784,7 @@ "isFicsmas": false }, { - "id": "CartridgeChaos_Packaged", + "id": "CartridgeChaos", "displayName": "Turbo Rifle Ammo", "ingredients": [ { @@ -9798,7 +9798,7 @@ "perMin": 15 }, { - "part": "PackagedTurboFuel", + "part": "LiquidTurboFuel", "amount": 3, "perMin": 15 } @@ -9812,14 +9812,14 @@ } ], "building": { - "name": "manufacturermk1", - "power": 55 + "name": "blender", + "power": 75 }, "isAlternate": false, "isFicsmas": false }, { - "id": "CartridgeChaos", + "id": "CartridgeChaos_Packaged", "displayName": "Turbo Rifle Ammo", "ingredients": [ { @@ -9833,7 +9833,7 @@ "perMin": 15 }, { - "part": "LiquidTurboFuel", + "part": "PackagedTurboFuel", "amount": 3, "perMin": 15 } @@ -9847,8 +9847,8 @@ } ], "building": { - "name": "blender", - "power": 75 + "name": "manufacturermk1", + "power": 55 }, "isAlternate": false, "isFicsmas": false diff --git a/web/src/config/config.ts b/web/src/config/config.ts index 5d73f53c..28e2a5c4 100644 --- a/web/src/config/config.ts +++ b/web/src/config/config.ts @@ -1,4 +1,4 @@ export const config = { apiUrl: import.meta.env.VITE_ENV === 'dev' ? 'http://localhost:3001' : 'https://api.satisfactory-factories.app', - dataVersion: '1.0-29', + dataVersion: '1.2-00', }