| \n"
363 |
364 | lines += f"Session Name: {saveFileInfo.sessionName} \n"
365 | lines += f"Save Date: {saveFileInfo.saveDatetime.strftime('%m/%d/%Y %I:%M:%S %p')} \n"
366 |
367 | SECONDS_IN_MINUTE = 60
368 | SECONDS_IN_HOUR = 60 * SECONDS_IN_MINUTE
369 | playTimeHours = int(saveFileInfo.playDurationInSeconds / SECONDS_IN_HOUR)
370 | playTimeMinutes = (saveFileInfo.playDurationInSeconds - playTimeHours * SECONDS_IN_HOUR) / SECONDS_IN_MINUTE
371 | lines += f'Play Time: {playTimeHours} hours, {round(playTimeMinutes,1)} minutes\n'
372 | if len(sav_parse.satisfactoryCalculatorInteractiveMapExtras):
373 | lines += f'File suspected of having been saved by satisfactory-calculator.com/en/interactive-map for {len(sav_parse.satisfactoryCalculatorInteractiveMapExtras)} reasons. \n'
374 |
375 | lines += f'Game Phase: {gamePhase} \n'
376 |
377 | if activeSchematicDescription != None:
378 | lines += f'Active Milestone: {activeSchematicDescription}'
379 |
380 | lines += f'Mining {numMinedResources} of {len(minedResourceActors)} resources'
381 | if creatingMapImagesFlag:
382 | lines += f' (map).\nMap of Power Lines. \n'
383 | else:
384 | lines += ". \n"
385 |
386 | TOTAL_NUM_SLUGS = len(sav_data_slug.POWER_SLUGS_BLUE) + len(sav_data_slug.POWER_SLUGS_YELLOW) + len(sav_data_slug.POWER_SLUGS_PURPLE)
387 | totalNumCollectedSlugs = numCollectedSlugsMk1 + numCollectedSlugsMk2 + numCollectedSlugsMk3
388 | lines += f"{totalNumCollectedSlugs} of {TOTAL_NUM_SLUGS} slugs collected.\n"
389 | lines += f"{numCollectedSlugsMk1} of {len(sav_data_slug.POWER_SLUGS_BLUE)} blue.\n"
390 | lines += f"{numCollectedSlugsMk2} of {len(sav_data_slug.POWER_SLUGS_YELLOW)} yellow.\n"
391 | lines += f"{numCollectedSlugsMk3} of {len(sav_data_slug.POWER_SLUGS_PURPLE)} purple."
392 | if creatingMapImagesFlag:
393 | lines += f'\nMap of remaining slugs.'
394 | lines += " \n"
395 |
396 | lines += f"{len(uncollectedSomersloops)} Somersloops remaining ({round(len(uncollectedSomersloops)/len(sav_data_somersloop.SOMERSLOOPS)*100,1)}% of {len(sav_data_somersloop.SOMERSLOOPS)})."
397 | if creatingMapImagesFlag:
398 | lines += f' map'
399 | lines += " \n"
400 |
401 | lines += f"{len(uncollectedMercerSpheres)} Mercer Spheres remaining ({round(len(uncollectedMercerSpheres)/len(sav_data_mercerSphere.MERCER_SPHERES)*100,1)}% of {len(sav_data_mercerSphere.MERCER_SPHERES)})."
402 | if creatingMapImagesFlag:
403 | lines += f' map'
404 | lines += " \n"
405 |
406 | numCrashSitesNotOpened = len(crashSiteInstances) - numOpenAndEmptyCrashSites - numOpenAndFullCrashSites
407 | lines += f"Of {len(crashSiteInstances)} crash sites, {numOpenAndEmptyCrashSites} {('have','has')[numOpenAndEmptyCrashSites == 1]} been looted, {numCrashSitesNotOpened} {('have','has')[numCrashSitesNotOpened == 1]} not been opened, {numOpenAndFullCrashSites} {('are','is')[numOpenAndFullCrashSites == 1]} open with a drive available.\r\n"
408 | if creatingMapImagesFlag:
409 | lines += f'Map of hard drives.'
410 | lines += ' \n'
411 |
412 | lines += "Unlock Progress:\n"
413 | lines += ' \n'
414 | lines += f"- HUB Tiers: {unlockCount_hubTiers} of {len(sav_parse.UNLOCK_PATHS__HUB_TIERS)}
\n"
415 | lines += f"- MAM: {unlockCount_mam} of {len(sav_parse.UNLOCK_PATHS__MAM)}
\n"
416 | #lines += str(unlockRemaining_mam) # Research_AO_Hog and Research_AO_Spitter remain, but they're clearly unlocked
417 | lines += f"- Awesome Shop: {unlockCount_awesomeShop} of {len(sav_parse.UNLOCK_PATHS__AWESOME_SHOP)}
\n"
418 | #lines += str(unlockRemaining_awesomeShop) # TODO: Why ResourceSink_Checkmark??? My calculation is the same as satisfactory-calculator, but not the game
419 | lines += f"- Hard Drives: {unlockCount_hardDrives} of {len(sav_parse.UNLOCK_PATHS__HARD_DRIVES)}
\n"
420 | lines += f"- Special: {unlockCount_special} of {len(sav_parse.UNLOCK_PATHS__SPECIAL)}
\n"
421 | lines += " \n"
422 |
423 | if len(pointProgressLines) > 0:
424 | lines += "Sink Point Progress:\n"
425 | lines += '\n'
426 | lines += pointProgressLines
427 | lines += " \n"
428 |
429 | if len(dimensionalDepotContents) > 0:
430 | lines += "Dimensional Depot Contains:\n"
431 | lines += '\n'
432 | dimensionalDepotContents.sort(key=lambda x: x[1])
433 | for (itemCount, itemName) in dimensionalDepotContents:
434 | stackSize = getStackSize(itemName, itemCount)
435 | if stackSize == None:
436 | lines += f"- {itemCount} x {itemName}
\n"
437 | else:
438 | lines += f"- {itemCount} x {itemName} ({round(itemCount/(stackSize*CURRENT_DEPOT_STACK_LIMIT)*100,1)}%)
\n"
439 | lines += " \n"
440 |
441 | if len(buildablesMap) > 0:
442 | lines += f"{numBuildables} current built items:\n"
443 | lines += '\n'
444 | buildables = []
445 | for buildable in buildablesMap:
446 | buildables.append((buildablesMap[buildable], buildable))
447 | buildables.sort(reverse=True, key=lambda x: x[0])
448 | for buildable in buildables:
449 | shortName = sav_parse.pathNameToReadableName(buildable[1])
450 | lines += f"- {buildable[0]} x {shortName}
\n"
451 | lines += " \n"
452 |
453 | if numCreaturesKilled > 0:
454 | lines += f"{numCreaturesKilled} creatures killed:\n"
455 | lines += '\n'
456 | creaturesKilled.sort(reverse=True, key=lambda x: x[1])
457 | for creature in creaturesKilled:
458 | shortName = sav_parse.pathNameToReadableName(creature[0])
459 | lines += f"- {creature[1]} x {shortName}
\n"
460 | lines += f"- Flying Crab Hatchers not tracked as of v1.0.0.4
\n"
461 | lines += " \n"
462 |
463 | lines += " |
\n"
464 | lines += "\n"
465 | lines += "\n"
466 |
467 | with open(htmlFilename, "w") as fout:
468 | fout.write(lines)
469 | chown(htmlFilename)
470 |
471 | if creatingMapImagesFlag:
472 | Image.MAX_IMAGE_PIXELS = 1700000000
473 |
474 | try:
475 | imageFont = ImageFont.truetype(FONT_FILENAME, MAP_FONT_SIZE)
476 | # Testing has shown that, if this font load fails, imageFont is None
477 | # which cases the drawn text to be present, just with a tiny font.
478 | # User feedback has reported that it threw OSError.
479 | except:
480 | imageFont = None
481 |
482 | if imageFont == None:
483 | print("CAUTION: An error occured loading the font file. Please update the FONT_FILENAME variable to point at a font installed on your system. If you need a .ttf file, can you can get from https://github.com/kiwi0fruit/open-fonts")
484 |
485 | origImage = Image.open(MAP_BASENAME_BLANK)
486 |
487 | slugImage = origImage.copy()
488 | slugDraw = ImageDraw.Draw(slugImage)
489 | addSlugs(slugDraw, uncollectedPowerSlugsBlue, (0,0,255))
490 | addSlugs(slugDraw, uncollectedPowerSlugsYellow, (255,255,0))
491 | addSlugs(slugDraw, uncollectedPowerSlugsPurple, (192,0,192))
492 | slugDraw.text(MAP_TEXT_1, saveFileInfo.saveDatetime.strftime("Slugs from save %m/%d/%Y %I:%M:%S %p"), font=imageFont, fill=(0,0,0))
493 | slugDraw.text(MAP_TEXT_2, saveFileInfo.sessionName, font=imageFont, fill=(0,0,0))
494 | imageFilename = f"{outputDir}/{MAP_BASENAME_SLUGS}"
495 | slugImage.crop(CROP_SETTINGS).save(imageFilename)
496 | chown(imageFilename)
497 |
498 | hdImage = origImage.copy()
499 | hdDraw = ImageDraw.Draw(hdImage)
500 | for key in crashSiteInstances:
501 | coord = crashSiteInstances[key]
502 | posX = adjPos(coord[0], False)
503 | posY = adjPos(coord[1], True)
504 | hdDraw.ellipse((posX-2, posY-2, posX+2, posY+2), fill=(255,255,255))
505 | for key in crashSitesUnopenedKeys:
506 | coord = crashSiteInstances[key]
507 | posX = adjPos(coord[0], False)
508 | posY = adjPos(coord[1], True)
509 | hdDraw.ellipse((posX-2, posY-2, posX+2, posY+2), fill=(0,0,255))
510 | for coord in crashSitesOpenWithDrive:
511 | if coord != None:
512 | posX = adjPos(coord[0], False)
513 | posY = adjPos(coord[1], True)
514 | hdDraw.ellipse((posX-2, posY-2, posX+2, posY+2), fill=(0,255,0))
515 | hdDraw.text(MAP_TEXT_1, saveFileInfo.saveDatetime.strftime("Hard drives from save %m/%d/%Y %I:%M:%S %p"), font=imageFont, fill=(0,0,0))
516 | hdDraw.text(MAP_TEXT_2, saveFileInfo.sessionName, font=imageFont, fill=(0,0,0))
517 | imageFilename = f"{outputDir}/{MAP_BASENAME_HARD_DRIVES}"
518 | hdImage.crop(CROP_SETTINGS).save(imageFilename)
519 | chown(imageFilename)
520 |
521 | ssImage = origImage.copy()
522 | ssDraw = ImageDraw.Draw(ssImage)
523 | for instanceName in uncollectedSomersloops:
524 | (rootObject, rotation, position) = uncollectedSomersloops[instanceName]
525 | posX = adjPos(position[0], False)
526 | posY = adjPos(position[1], True)
527 | ssDraw.ellipse((posX-2, posY-2, posX+2, posY+2), fill=(244,56,69))
528 | ssDraw.text(MAP_TEXT_1, saveFileInfo.saveDatetime.strftime("Somersloops from save %m/%d/%Y %I:%M:%S %p"), font=imageFont, fill=(0,0,0))
529 | ssDraw.text(MAP_TEXT_2, saveFileInfo.sessionName, font=imageFont, fill=(0,0,0))
530 | imageFilename = f"{outputDir}/{MAP_BASENAME_SOMERSLOOP}"
531 | ssImage.crop(CROP_SETTINGS).save(imageFilename)
532 | chown(imageFilename)
533 |
534 | msImage = origImage.copy()
535 | msDraw = ImageDraw.Draw(msImage)
536 | for instanceName in uncollectedMercerSpheres:
537 | (rootObject, rotation, position) = uncollectedMercerSpheres[instanceName]
538 | posX = adjPos(position[0], False)
539 | posY = adjPos(position[1], True)
540 | msDraw.ellipse((posX-2, posY-2, posX+2, posY+2), fill=(78,16,113))
541 | msDraw.text(MAP_TEXT_1, saveFileInfo.saveDatetime.strftime("Mercer Spheres from save %m/%d/%Y %I:%M:%S %p"), font=imageFont, fill=(0,0,0))
542 | msDraw.text(MAP_TEXT_2, saveFileInfo.sessionName, font=imageFont, fill=(0,0,0))
543 | imageFilename = f"{outputDir}/{MAP_BASENAME_MERCER_SPHERE}"
544 | msImage.crop(CROP_SETTINGS).save(imageFilename)
545 | chown(imageFilename)
546 |
547 | plImage = origImage.copy()
548 | plDraw = ImageDraw.Draw(plImage)
549 | for (src, dst) in wireLines:
550 | possX = adjPos(src[0], False)
551 | posdX = adjPos(dst[0], False)
552 | possY = adjPos(src[1], True)
553 | posdY = adjPos(dst[1], True)
554 | plDraw.line(((possX, possY), (posdX, posdY)), fill=(22,47,101), width=2)
555 | plDraw.text(MAP_TEXT_1, saveFileInfo.saveDatetime.strftime("Power Lines from save %m/%d/%Y %I:%M:%S %p"), font=imageFont, fill=(0,0,0))
556 | plDraw.text(MAP_TEXT_2, saveFileInfo.sessionName, font=imageFont, fill=(0,0,0))
557 | imageFilename = f"{outputDir}/{MAP_BASENAME_POWER}"
558 | plImage.crop(CROP_SETTINGS).save(imageFilename)
559 | chown(imageFilename)
560 |
561 | rnImage = origImage.copy()
562 | rnDraw = ImageDraw.Draw(rnImage)
563 | for instanceName in minedResourceActors:
564 | (position, type, purity) = minedResourceActors[instanceName]
565 | posX = adjPos(position[0], False)
566 | posY = adjPos(position[1], True)
567 |
568 | sz = 3
569 | if instanceName in minedResources:
570 | sz = 2
571 |
572 | purityColors = {
573 | sav_parse.Purity.IMPURE: (210,52,48),
574 | sav_parse.Purity.NORMAL: (242,100,24),
575 | sav_parse.Purity.PURE: (128,177,57),
576 | }
577 | if purity in purityColors:
578 | rnDraw.ellipse((posX-sz, posY-sz, posX+sz, posY+sz), fill=purityColors[purity])
579 |
580 | sz -= 1
581 |
582 | typeColors = {
583 | None: (255,255,255),
584 | "Desc_Coal_C": (80,80,80),
585 | "Desc_Geyser_C": (192,192,255),
586 | "Desc_LiquidOil_C": (20,20,20),
587 | "Desc_LiquidOilWell_C": (20,20,20),
588 | "Desc_NitrogenGas_C": (232,229,196),
589 | "Desc_OreBauxite_C": (200,140,114),
590 | "Desc_OreCopper_C": (149,93,87),
591 | "Desc_OreGold_C": (210,188,150),
592 | "Desc_OreIron_C": (111,80,93),
593 | "Desc_OreUranium_C": (94,141,82),
594 | "Desc_RawQuartz_C": (221,154,201),
595 | "Desc_SAM_C": (110,46,169),
596 | "Desc_Stone_C": (191,178,168),
597 | "Desc_Sulfur_C": (205,191,102),
598 | "Desc_Water_C": (165,204,223),
599 | }
600 | if type in typeColors:
601 | rnDraw.ellipse((posX-sz, posY-sz, posX+sz, posY+sz), fill=typeColors[type])
602 | rnDraw.text(MAP_TEXT_1, saveFileInfo.saveDatetime.strftime("Resource Nodes from save %m/%d/%Y %I:%M:%S %p"), font=imageFont, fill=(0,0,0))
603 | rnDraw.text(MAP_TEXT_2, saveFileInfo.sessionName, font=imageFont, fill=(0,0,0))
604 | imageFilename = f"{outputDir}/{MAP_BASENAME_RESOURCE_NODES}"
605 | rnImage.crop(CROP_SETTINGS).save(imageFilename)
606 | chown(imageFilename)
607 |
608 | except Exception as error:
609 | with open(htmlFilename, "w") as fout:
610 | fout.write(f"