Skip to content

Instantly share code, notes, and snippets.

@glcoder
Last active October 30, 2023 19:03
Show Gist options
  • Save glcoder/44f33a9d1b5b9618a44fbe81af9ebda0 to your computer and use it in GitHub Desktop.
Save glcoder/44f33a9d1b5b9618a44fbe81af9ebda0 to your computer and use it in GitHub Desktop.
Generate DSP data.json for factorio-lab using extracted by uTinyRipper assets.
#!/usr/bin/python
import os
import sys
import yaml
import json
import pprint
import textwrap
import unityparser
from skimage import io
BASE_PATH = os.path.abspath(os.path.dirname(sys.argv[0]))
RESOURCES_PATH = os.path.join(BASE_PATH, "DSPGAME", "Assets", "Resources")
PROTOTYPES_PATH = os.path.join(RESOURCES_PATH, "prototypes")
TEXTURES_PATH = os.path.join(RESOURCES_PATH, "ui", "textures")
# This command will produce icons.png
'''
magick montage ^
DSPGAME/Assets/Resources/ui/textures/sprites/icons/component-icon.png ^
DSPGAME/Assets/Resources/ui/textures/sprites/icons/factory-icon.png ^
DSPGAME/Assets/Resources/icons/tech/1606.png ^
DSPGAME/Assets/Resources/icons/itemrecipe/*.png ^
DSPGAME/Assets/Resources/icons/vein/*.png ^
-geometry 64x64+0+0 -background transparent ^
png32:icons.png
'''
NAMES = {}
IDS = {}
GRID = {}
ITEMS = {}
RECIPES = {}
PREFABS = {}
VEINS = {}
ICONS = {}
WATER_PUMP_ITEMS = [
"water",
"sulphuric-acid",
]
RECIPE_TYPES = {
0: "None",
1: "Smelt",
2: "Chemical",
3: "Refine",
4: "Assemble",
5: "Particle",
6: "Exchange",
7: "PhotonStore",
8: "Fractionate",
15: "Research",
}
MINER_TYPES = {
0: "None",
1: "Water",
2: "Vein",
3: "Oil",
}
FUEL_TYPES = {
0: 'None',
1: 'Chemical',
2: 'Nuclear',
4: "Antimatter",
8: 'Accumulator',
}
PRODUCERS = {
'Smelt': ["smelter"],
'Chemical': ["chemical-plant"],
'Refine': ["oil-refinery"],
'Assemble': ["assembler-1", "assembler-2", "assembler-3"],
'Particle': ["hadron-collider"],
'Fractionate': ["fractionator"],
'Research': ["lab"],
'Water': ["water-pump"],
'Vein': ["mining-drill"],
'Oil': ["oil-extractor"],
}
def GetName(Name):
return NAMES[Name] if Name in NAMES else Name
def ToItemId(Path):
return Path.split("/")[-1].lower()
def ToIntArray(Input):
def Convert(x): return int.from_bytes(bytes.fromhex(x), byteorder='little')
return [Convert(x) for x in textwrap.wrap(str(Input), 8)]
def ListIcons(IconsPath):
Icons = {}
for Filename in os.listdir(IconsPath):
if Filename.endswith(".png"):
IconId = Filename[:-4].lower()
Icons[IconId] = os.path.join(IconsPath, Filename)
return Icons
def ParseBeltDesc(Desc):
return {
'beltSpeed': Desc['speed']
}
def ParseAssemblerDesc(Desc):
return {
'assemblerType': Desc['recipeType'],
'assemblerSpeed': Desc['speedf'],
}
def ParseLabDesc(Desc):
return {
'assembleSpeed': Desc['assembleSpeed'],
'researchSpeed': Desc['researchSpeed'],
}
def ParseMinerDesc(Desc):
return {
'minerType': Desc['minerType'],
'miningSpeed': Desc['periodf'],
}
def ParseFractionateDesc(Desc):
return {
'fractionateType': Desc['recipeType'],
'needMaxCount': Desc['needMaxCount'],
'productMaxCount': Desc['productMaxCount'],
'oriProductMaxCount': Desc['oriProductMaxCount'],
}
def ParsePowerDesc(Desc):
return {
'fuelMask': Desc['fuelMask'],
'productId': Desc['productId'],
'workEnergyPerTick': Desc['workEnergyPerTick'],
'idleEnergyPerTick': Desc['idleEnergyPerTick'],
}
def ParseInserterDesc(Desc):
return {
'inserterCanStack': Desc['canStack'],
'inserterStackSize': Desc['stackSize'],
}
def ParseStationDesc(Desc):
if Desc['isCollector']:
return {
'stationCollectSpeed': Desc['collectSpeed']
}
return {}
SCRIPT_TYPES = {
'f4df938feead02b4a9ac37389ccb926d': ParseBeltDesc,
'0e3d842d96462e9459f1989fd341389e': ParseAssemblerDesc,
'b8894acb258f3c749bd3046541329178': ParsePowerDesc,
'89e018bb65efaf848958841d52240534': ParseLabDesc,
'8dce58da3c359c14da23e9f8106cdca2': ParseMinerDesc,
'1f03319d47b5d2a4798e64a8cf2a994c': ParseFractionateDesc,
'a883c8cc7e279dc44b151589ae9f5c5d': ParseInserterDesc,
'8fef6d3ce26c11d48ba2d9b5fe033a3e': ParseStationDesc,
}
PREFAB_NAME_PROXY = {
'assembler-mk-1': "assembler-1",
'assembler-mk-2': "assembler-2",
'assembler-mk-3': "assembler-3",
'power-generator': "fuel-plant",
}
UI_ICONS_PATH = os.path.join(TEXTURES_PATH, "sprites", "icons")
ICNOS_PATH = os.path.join(RESOURCES_PATH, "icons")
ICON_PATHS = {
'components': os.path.join(UI_ICONS_PATH, "component-icon.png"),
'buildings': os.path.join(UI_ICONS_PATH, "factory-icon.png"),
'gas-mining-tech': os.path.join(ICNOS_PATH, "tech", "1606.png"),
}
ICON_PATHS.update(ListIcons(os.path.join(ICNOS_PATH, "itemrecipe")))
ICON_PATHS.update(ListIcons(os.path.join(ICNOS_PATH, "vein")))
ICON_INDEX = 0
for IconId, IconPath in ICON_PATHS.items():
Image = io.imread(IconPath)[:, :, :-1]
Average = Image.mean(axis=0).mean(axis=0)
Icon = {
"color": "#%02X%02X%02X" % (int(Average[0]), int(Average[1]), int(Average[2])),
"id": IconId,
"position": "%dpx %dpx" % (-64 * int(ICON_INDEX % 13), -64 * int(ICON_INDEX / 13))
}
ICONS[IconId] = Icon
ICON_INDEX = ICON_INDEX + 1
for Item in WATER_PUMP_ITEMS:
VeinId = Item + "-vein"
ICONS[VeinId] = {**ICONS[Item], 'id': VeinId}
with open(os.path.join(PROTOTYPES_PATH, "StringProtoSet.asset"), "r", encoding='utf8') as stream:
try:
Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader)
for Entry in Document.dataArray:
NAMES[Entry['Name']] = Entry['ENUS']
except yaml.YAMLError as e:
print(e)
with open(os.path.join(PROTOTYPES_PATH, "ItemProtoSet.asset"), "r", encoding='utf8') as stream:
try:
Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader)
for Entry in Document.dataArray:
Name = GetName(Entry['Name'])
ItemId = ToItemId(Entry['IconPath'])
IsBuilding = bool(Entry['IsEntity']) and bool(Entry['CanBuild'])
Item = {
'category': "buildings" if IsBuilding else "components",
'id': ItemId,
'name': Name,
'row': int((Entry['GridIndex'] % 1000) / 100 - 1),
'stack': int(Entry['StackSize']),
}
if (Entry['FuelType'] > 0):
Item['fuel'] = {
'category': FUEL_TYPES[Entry['FuelType']].lower(),
'value': float(Entry['HeatValue']) / 1000000.0,
}
IDS[Entry['ID']] = ItemId
GRID[Entry['GridIndex']] = ItemId
ITEMS[ItemId] = Item
except yaml.YAMLError as e:
print(e)
with open(os.path.join(PROTOTYPES_PATH, "RecipeProtoSet.asset"), "r", encoding='utf8') as stream:
try:
Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader)
for Entry in Document.dataArray:
Name = GetName(Entry['Name'])
Items = [IDS[x] for x in ToIntArray(Entry['Items'])]
ItemCounts = ToIntArray(Entry['ItemCounts'])
Results = [IDS[x] for x in ToIntArray(Entry['Results'])]
ResultCounts = ToIntArray(Entry['ResultCounts'])
if bool(Entry['Explicit']):
RecipeId = ToItemId(Entry['IconPath'])
else:
RecipeId = next(iter(Results))
Time = float(Entry['TimeSpend']) / 60.0
if RECIPE_TYPES[Entry['Type']] == "Fractionate":
Time = 1.0
Fraction = float(ResultCounts[0]) / float(ItemCounts[0])
ItemCounts[0] = 1.0
ResultCounts[0] = Fraction
Results.append(Items[0])
ResultCounts.append(1.0 - Fraction)
RECIPES[RecipeId] = {
'id': RecipeId,
'name': Name,
'in': dict(zip(Items, ItemCounts)),
'out': dict(zip(Results, ResultCounts)),
'time': Time,
'producers': PRODUCERS[RECIPE_TYPES[Entry['Type']]],
}
except yaml.YAMLError as e:
print(e)
with open(os.path.join(PROTOTYPES_PATH, "VeinProtoSet.asset"), "r", encoding='utf8') as stream:
try:
Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader)
for Entry in Document.dataArray:
VeinId = ToItemId(Entry['IconPath'])
IsVein = bool(Entry['MinerBaseModelIndex'])
Vein = {
'id': VeinId,
'type': "Vein" if IsVein else "Oil",
'out': {IDS[Entry['MiningItem']]: 1},
}
VEINS[VeinId] = Vein
except yaml.YAMLError as e:
print(e)
for Item in WATER_PUMP_ITEMS:
VeinId = Item + "-vein"
VEINS[VeinId] = {
'id': VeinId,
'type': "Water",
'out': {Item: 1},
}
PREFAB_PATH = os.path.join(RESOURCES_PATH, "entities", "prefabs")
for FileName in os.listdir(PREFAB_PATH):
if not FileName.endswith(".prefab"):
continue
Name = FileName[:-7]
PrefabId = PREFAB_NAME_PROXY[Name] if Name in PREFAB_NAME_PROXY else Name
Prefab = {}
with open(os.path.join(PREFAB_PATH, FileName), "r", encoding='utf8') as stream:
try:
Docs = yaml.load_all(stream, Loader=unityparser.loader.UnityLoader)
for Entry in [Entry.get_serialized_properties_dict() for Entry in Docs]:
if 'm_Script' in Entry:
Guid = Entry['m_Script']['guid']
if Guid in SCRIPT_TYPES:
Prefab.update(SCRIPT_TYPES[Guid](Entry))
except yaml.YAMLError as e:
print(e)
PREFABS[PrefabId] = Prefab
for ItemId, Prefab in PREFABS.items():
if not ItemId in ITEMS:
continue
if 'beltSpeed' in Prefab:
ITEMS[ItemId]['belt'] = {
'speed': float(Prefab['beltSpeed']) * 6.0
}
ITEMS[ItemId]['module'] = {
'consumption': 0.0,
'speed': float(Prefab['beltSpeed']) - 1.0
}
Factory = {}
if 'assemblerSpeed' in Prefab:
Factory['speed'] = float(Prefab['assemblerSpeed'])
if 'workEnergyPerTick' in Prefab and Prefab['workEnergyPerTick'] > 0:
Factory['type'] = "electric"
Factory['usage'] = float(Prefab['workEnergyPerTick']) * 0.06
if 'idleEnergyPerTick' in Prefab and Prefab['idleEnergyPerTick'] > 0:
Factory['type'] = "electric"
Factory['drain'] = float(Prefab['idleEnergyPerTick']) * 0.06
if 'fuelMask' in Prefab and Prefab['fuelMask'] > 0:
Factory['type'] = "burner"
Factory['category'] = FUEL_TYPES[Prefab['fuelMask']].lower()
if 'minerType' in Prefab:
Factory['mining'] = True
Factory['speed'] = 1.0
if 'assembleSpeed' in Prefab:
Factory['speed'] = float(Prefab['assembleSpeed'])
if 'productId' in Prefab and Prefab['productId'] > 0:
Factory['speed'] = 1.0
if 'fractionateType' in Prefab:
Factory['modules'] = 1
Factory['speed'] = 6.0 # belt speed multiplier
if 'stationCollectSpeed' in Prefab:
Factory['mining'] = True
Factory['speed'] = float(Prefab['stationCollectSpeed'])
if 'minerType' in Prefab and Prefab['minerType'] > 0:
del Factory['type']
del Factory['usage']
del Factory['drain']
if len(Factory) > 0:
Factory['speed'] = Factory['speed'] if 'speed' in Factory else 1.0
ITEMS[ItemId]['factory'] = Factory
if 'productId' in Prefab and Prefab['productId'] > 0:
RecipeId = IDS[Prefab['productId']]
RECIPES[RecipeId] = {
'id': RecipeId,
'mining': True,
'out': {RecipeId: 1},
'time': 1.0,
'producers': [ItemId],
}
if 'minerType' in Prefab and Prefab['minerType'] > 0:
MinerType = MINER_TYPES[Prefab['minerType']]
for VeinId, Vein in VEINS.items():
if Vein['type'] != MinerType:
continue
RECIPES[VeinId] = {
'id': VeinId,
'mining': True,
'time': float(Prefab['miningSpeed']),
'out': Vein['out'],
'producers': PRODUCERS[Vein['type']],
}
with open(os.path.join(PROTOTYPES_PATH, "ThemeProtoSet.asset"), "r", encoding='utf8') as stream:
try:
Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader)
for Entry in Document.dataArray:
if Entry['PlanetType'] != 5:
continue
PlanetType = 'gas' if Entry['Temperature'] > 0 else 'ice'
RecipeId = PlanetType + "-giant"
if RecipeId in RECIPES:
continue
GasItems = [IDS[x] for x in ToIntArray(Entry['GasItems'])]
GasSpeeds = [float(x) for x in Entry['GasSpeeds']]
TotalHeat = sum([Speed * ITEMS[ItemId]['fuel']['value']
for ItemId, Speed in zip(GasItems, GasSpeeds)])
Producer = ITEMS['orbital-collector']
WorkEnergy = float(Producer['factory']['usage']) / 1000.0
CollectorSpeed = float(Producer['factory']['speed'])
Coefficient = 1.0 - WorkEnergy / (CollectorSpeed * TotalHeat)
GasCounts = [round(Speed * CollectorSpeed * Coefficient, 2)
for Speed in GasSpeeds]
RECIPES[RecipeId] = {
'id': RecipeId,
'name': GetName(Entry['DisplayName']),
'mining': True,
'time': CollectorSpeed,
'out': dict(zip(GasItems, GasCounts)),
'producers': [Producer['id']],
}
ICONS[RecipeId] = {**ICONS['gas-mining-tech'], 'id': RecipeId}
except yaml.YAMLError as e:
print(e)
DATA = {
'categories': [
{'id': "components", 'name': "Components"},
{'id': "buildings", 'name': "Buildings"},
],
'icons': list(ICONS.values()),
'items': [ITEMS[ItemId] for _, ItemId in sorted(GRID.items())],
'recipes': list(RECIPES.values()),
'defaults': {
'modIds': [],
'minBelt': "belt-1",
'maxBelt': "belt-3",
'fuel': "coal-ore",
'disabledRecipes': [],
'minFactoryRank': ["assembler-1"],
'maxFactoryRank': ["assembler-3"],
'moduleRank': []
},
'limitations': {'productivity-module': []}
}
print(json.dumps(DATA))
scikit-image>=0.18.1
PyYAML>=5.1
unityparser>=1.0.0
@starkos0
Copy link

Hey, i tried to use this script but it throws me an error just when i use .load from yaml library, do you have any idea why?
Exception has occurred: AttributeError
'NoneType' object has no attribute 'set'
Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader)
Looks like it doesn't recognize the file but i'm not sure why

@glcoder
Copy link
Author

glcoder commented Oct 26, 2023

Hey, i tried to use this script but it throws me an error just when i use .load from yaml library, do you have any idea why? Exception has occurred: AttributeError 'NoneType' object has no attribute 'set' Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader) Looks like it doesn't recognize the file but i'm not sure why

Hi, Unity YAML is not a strict YAML as I remember, you need to fix source code of YAML parser or fix Unity files. I don't know what I did myself exactly, maybe preprocessed Unity files first to edit invalid lines. I really don't remember.

@starkos0
Copy link

Hey, i tried to use this script but it throws me an error just when i use .load from yaml library, do you have any idea why? Exception has occurred: AttributeError 'NoneType' object has no attribute 'set' Document = yaml.load(stream, Loader=unityparser.loader.UnityLoader) Looks like it doesn't recognize the file but i'm not sure why

Hi, Unity YAML is not a strict YAML as I remember, you need to fix source code of YAML parser or fix Unity files. I don't know what I did myself exactly, maybe preprocessed Unity files first to edit invalid lines. I really don't remember.

Ty for the hint, i fixed it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment