477 lines
16 KiB
Plaintext
477 lines
16 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# About\n",
|
|
"\n",
|
|
"This is a notebook for finding and copying Textures form extracted game assets to named images for the item database. \n",
|
|
"\n",
|
|
"## Why not a script?\n",
|
|
"\n",
|
|
"because depending on what extractor you use and the whims of the developers all this could use some serious tweaking every run. The notebook lets you run things in stages and inspect what your working with."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"%matplotlib widget\n",
|
|
"import json\n",
|
|
"\n",
|
|
"database = {}\n",
|
|
"\n",
|
|
"with open(\"src/ts/virtualMachine/prefabDatabase.ts\", \"r\") as f:\n",
|
|
" contents = f.read().removeprefix(\"export default \").removesuffix(\" as const\")\n",
|
|
" database = json.loads(contents)\n",
|
|
"\n",
|
|
"db = database[\"prefabs\"]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Item Database Pulled in"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from pathlib import Path \n",
|
|
"\n",
|
|
"# Location were https://github.com/SeriousCache/UABE has extracted all Texture2D assets\n",
|
|
"# Change as necessary\n",
|
|
"datapath = Path(r\"E:\\Games\\SteamLibrary\\steamapps\\common\\Stationeers\\Stationpedia\\exported_textures\")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Change this Datapath to point to the extracted textures"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 3,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import os\n",
|
|
"\n",
|
|
"# Pull in a list of all found textures\n",
|
|
"images = list(datapath.glob(\"*.png\"))\n",
|
|
"names = [image.name for image in images]\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Finding matches\n",
|
|
"\n",
|
|
"This next section loops through all the item names and collects all the candidate textures. Then, through a series of rules, attempts to narrow down the choices to 1 texture."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 35,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"image_candidates = {}\n",
|
|
"\n",
|
|
"def filter_candidates(candidates):\n",
|
|
" max_match_len = 0\n",
|
|
" filtered_matches = []\n",
|
|
"\n",
|
|
" # go for longest match\n",
|
|
" for can in candidates:\n",
|
|
" name, match, mapping = can\n",
|
|
" match_len = len(match)\n",
|
|
" if match_len > max_match_len:\n",
|
|
" max_match_len = match_len\n",
|
|
" filtered_matches = [(name, match, mapping)]\n",
|
|
" elif match_len == max_match_len:\n",
|
|
" filtered_matches.append((name, match, mapping))\n",
|
|
"\n",
|
|
" # choose better matches\n",
|
|
" if len(filtered_matches) > 1:\n",
|
|
" better_matches = []\n",
|
|
" for can in filtered_matches:\n",
|
|
" name, match, mapping = can\n",
|
|
" if mapping.startswith(\"Item\") and mapping in name or mapping.lower() in name:\n",
|
|
" better_matches.append((name, match, mapping))\n",
|
|
" elif mapping.startswith(\"Structure\") and mapping in name or mapping.lower() in name:\n",
|
|
" better_matches.append((name, match, mapping))\n",
|
|
" if len(better_matches) > 0:\n",
|
|
" filtered_matches = better_matches\n",
|
|
"\n",
|
|
" #exclude build states if we have non build states\n",
|
|
" if len(filtered_matches) > 1:\n",
|
|
" non_build_state = []\n",
|
|
" for can in filtered_matches:\n",
|
|
" name, match, mapping = can\n",
|
|
" if \"BuildState\" not in name:\n",
|
|
" non_build_state.append((name, match, mapping))\n",
|
|
" if len(non_build_state) > 0:\n",
|
|
" filtered_matches = non_build_state\n",
|
|
"\n",
|
|
" #prefer matches without extra tags\n",
|
|
" if len(filtered_matches) > 1:\n",
|
|
" direct = []\n",
|
|
" for can in filtered_matches:\n",
|
|
" name, match, mapping = can\n",
|
|
" if f\"{match}-\" in name or f\"{match.lower()}-\" in name:\n",
|
|
" direct.append((name, match, mapping))\n",
|
|
" if len(direct) > 0:\n",
|
|
" filtered_matches = direct\n",
|
|
" \n",
|
|
" #filter to unique filenames\n",
|
|
" if len(filtered_matches) > 1:\n",
|
|
" unique_names = []\n",
|
|
" unique_matches = []\n",
|
|
" for can in filtered_matches:\n",
|
|
" name, match, mapping = can\n",
|
|
" if name not in unique_names:\n",
|
|
" unique_names.append(name)\n",
|
|
" unique_matches.append((name, match, mapping))\n",
|
|
" filtered_matches = unique_matches\n",
|
|
"\n",
|
|
" #prefer not worse matches\n",
|
|
" if len(filtered_matches) > 1:\n",
|
|
" not_worse = []\n",
|
|
" for can in filtered_matches:\n",
|
|
" name, match, mapping = can\n",
|
|
" if name.startswith(\"Item\") and not mapping.startswith(\"Item\"):\n",
|
|
" continue\n",
|
|
" elif name.startswith(\"Structure\") and not mapping.startswith(\"Structure\"):\n",
|
|
" continue\n",
|
|
" elif name.startswith(\"Kit\") and not mapping.startswith(\"Kit\"):\n",
|
|
" continue\n",
|
|
" elif not (name.startswith(match) or name.startswith(match.lower())):\n",
|
|
" continue\n",
|
|
" not_worse.append((name, match, mapping))\n",
|
|
" if len(not_worse) > 0:\n",
|
|
" filtered_matches = not_worse\n",
|
|
"\n",
|
|
" #if we have colored variants take White\n",
|
|
" if len(filtered_matches) > 1:\n",
|
|
" for can in filtered_matches:\n",
|
|
" name, match, mapping = can\n",
|
|
" if f\"_White\" in name:\n",
|
|
" return [name]\n",
|
|
"\n",
|
|
" return [name for name, _, _ in filtered_matches]\n",
|
|
"\n",
|
|
"for entry in db.values():\n",
|
|
" candidates = []\n",
|
|
" entry_name = entry[\"prefab\"][\"prefab_name\"]\n",
|
|
" for name in names:\n",
|
|
" if entry_name in name or entry_name.lower() in name:\n",
|
|
" candidates.append((name, entry_name, entry_name))\n",
|
|
" if entry_name.removeprefix(\"Item\") in name or entry_name.removeprefix(\"Item\").lower() in name:\n",
|
|
" candidates.append((name, entry_name.removeprefix(\"Item\"), entry_name))\n",
|
|
" if entry_name.removeprefix(\"Structure\") in name or entry_name.removeprefix(\"Structure\").lower() in name:\n",
|
|
" candidates.append((name, entry_name.removeprefix(\"Structure\"), entry_name))\n",
|
|
" image_candidates[entry_name] = filter_candidates(candidates)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Some Items end up with no match but these items are often subtypes of an item that will have a match"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 36,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# rematch items to super structure?\n",
|
|
"for name in image_candidates.keys():\n",
|
|
" for other in image_candidates.keys():\n",
|
|
" if name != other and name in other:\n",
|
|
" if len(image_candidates[name]) > 0 and len(image_candidates[other]) == 0:\n",
|
|
" image_candidates[other] = image_candidates[name]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"Prepare out List of file copies. at this point a few items will never have a match. and one or two will have two choices but those choices will be filtered again by passing them through some extra function."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 40,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"CartridgePlantAnalyser []\n",
|
|
"Flag_ODA_10m []\n",
|
|
"Flag_ODA_4m []\n",
|
|
"Flag_ODA_6m []\n",
|
|
"Flag_ODA_8m []\n",
|
|
"ItemBiomass []\n",
|
|
"ItemHorticultureBelt []\n",
|
|
"ItemKitLiquidRegulator []\n",
|
|
"ItemKitPortablesConnector []\n",
|
|
"ItemPlantEndothermic_Creative []\n",
|
|
"ItemPlantThermogenic_Creative []\n",
|
|
"ItemSuitModCryogenicUpgrade []\n",
|
|
"Landingpad_GasConnectorInwardPiece []\n",
|
|
"Landingpad_LiquidConnectorInwardPiece []\n",
|
|
"StructureBlocker []\n",
|
|
"StructureElevatorLevelIndustrial []\n",
|
|
"StructureLogicSorter []\n",
|
|
"StructurePlinth []\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"%matplotlib widget\n",
|
|
"to_copy = []\n",
|
|
"\n",
|
|
"from IPython.display import Image, display\n",
|
|
"\n",
|
|
"gases = [(\"Oxygen\", \"White\"), (\"Nitrogen\", \"Black\"), (\"CarbonDioxide\", \"Yellow\"), (\"Fuel\", \"Orange\")]\n",
|
|
"\n",
|
|
"colors = [\"White\", \"Black\", \"Gray\", \"Khaki\", \"Brown\", \"Orange\", \"Yellow\", \"Red\", \"Green\", \"Blue\", \"Purple\"]\n",
|
|
"\n",
|
|
"def split_gas(name):\n",
|
|
" for gas, color in gases:\n",
|
|
" if name.endswith(gas):\n",
|
|
" return (name.removesuffix(gas), gas, color)\n",
|
|
" elif name.lower().endswith(gas):\n",
|
|
" return (name.lower().removesuffix(gas), gas, color)\n",
|
|
" elif name.lower().endswith(gas.lower()):\n",
|
|
" return (name.lower().removesuffix(gas.lower()), gas.lower(), color.lower())\n",
|
|
" return [name, None, None]\n",
|
|
"\n",
|
|
"def match_gas_color(name, candidates):\n",
|
|
" mat, gas, color = split_gas(name)\n",
|
|
" seek = f\"{mat}_{color}\"\n",
|
|
" if gas is not None:\n",
|
|
" for candidate in candidates:\n",
|
|
" if seek in candidate or seek.lower() in candidate:\n",
|
|
" return candidate\n",
|
|
" return None\n",
|
|
"\n",
|
|
"def prefer_direct(name, candidates):\n",
|
|
" for candidate in candidates:\n",
|
|
" if f\"{name}-\" in candidate or f\"{name.lower()}-\" in candidate:\n",
|
|
" return candidate\n",
|
|
" return None\n",
|
|
"\n",
|
|
"def prefer_uncolored(name, candidates):\n",
|
|
" for candidate in candidates:\n",
|
|
" for color in colors:\n",
|
|
" if f\"_{color}\" not in candidate and f\"_{color.lower()}\" not in candidate:\n",
|
|
" return candidate\n",
|
|
" return None\n",
|
|
"\n",
|
|
"def prefer_lower_match(name, candidates):\n",
|
|
" for candidate in candidates:\n",
|
|
" if f\"{name.lower()}-\" in candidate:\n",
|
|
" return candidate\n",
|
|
" return None\n",
|
|
"\n",
|
|
"filter_funcs = [prefer_lower_match, prefer_direct, match_gas_color, prefer_uncolored]\n",
|
|
"\n",
|
|
"for name, candidates in image_candidates.items():\n",
|
|
" if len(candidates) != 1:\n",
|
|
" found = False\n",
|
|
" for func in filter_funcs:\n",
|
|
" candidate = func(name, candidates)\n",
|
|
" if candidate is not None:\n",
|
|
" to_copy.append((name, candidate))\n",
|
|
" found = True\n",
|
|
" if found:\n",
|
|
" continue\n",
|
|
" print(name, candidates)\n",
|
|
" if len(candidates) > 1:\n",
|
|
" for candidate in candidates:\n",
|
|
" print(candidate)\n",
|
|
" display(Image(datapath / candidate))\n",
|
|
" \n",
|
|
" #take first as fallback\n",
|
|
" # to_copy.append((name, candidates[0]))\n",
|
|
" raise StopExecution\n",
|
|
" else:\n",
|
|
" # print(name, candidates)\n",
|
|
" to_copy.append((name, candidates[0]))\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 41,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"missing Egg\n",
|
|
"missing Appliance\n",
|
|
"missing Ingot\n",
|
|
"missing Torpedo\n",
|
|
"missing Magazine\n",
|
|
"missing SensorProcessingUnit\n",
|
|
"missing LiquidCanister\n",
|
|
"missing LiquidBottle\n",
|
|
"missing Wreckage\n",
|
|
"missing SoundCartridge\n",
|
|
"missing DrillHead\n",
|
|
"missing ScanningHead\n",
|
|
"missing Flare\n",
|
|
"missing Blocked\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"slot_types = [\n",
|
|
" \"Helmet\",\n",
|
|
" \"Suit\",\n",
|
|
" \"Back\",\n",
|
|
" \"GasFilter\",\n",
|
|
" \"GasCanister\",\n",
|
|
" \"MotherBoard\",\n",
|
|
" \"Circuitboard\",\n",
|
|
" \"DataDisk\",\n",
|
|
" \"Organ\",\n",
|
|
" \"Ore\",\n",
|
|
" \"Plant\",\n",
|
|
" \"Uniform\",\n",
|
|
" \"Entity\",\n",
|
|
" \"Battery\",\n",
|
|
" \"Egg\",\n",
|
|
" \"Belt\",\n",
|
|
" \"Tool\",\n",
|
|
" \"Appliance\",\n",
|
|
" \"Ingot\",\n",
|
|
" \"Torpedo\",\n",
|
|
" \"Cartridge\",\n",
|
|
" \"AccessCard\",\n",
|
|
" \"Magazine\",\n",
|
|
" \"Circuit\",\n",
|
|
" \"Bottle\",\n",
|
|
" \"ProgrammableChip\",\n",
|
|
" \"Glasses\",\n",
|
|
" \"CreditCard\",\n",
|
|
" \"DirtCanister\",\n",
|
|
" \"SensorProcessingUnit\",\n",
|
|
" \"LiquidCanister\",\n",
|
|
" \"LiquidBottle\",\n",
|
|
" \"Wreckage\",\n",
|
|
" \"SoundCartridge\",\n",
|
|
" \"DrillHead\",\n",
|
|
" \"ScanningHead\",\n",
|
|
" \"Flare\",\n",
|
|
" \"Blocked\",\n",
|
|
"]\n",
|
|
"sloticons = []\n",
|
|
"for typ in slot_types:\n",
|
|
" try_name = f\"sloticon_{typ.lower()}\"\n",
|
|
" found = False\n",
|
|
" for name in names:\n",
|
|
" if name.startswith(try_name):\n",
|
|
" sloticons.append([f\"SlotIcon_{typ}\", name])\n",
|
|
" found = True\n",
|
|
" if not found:\n",
|
|
" print(f\"missing {typ}\")\n",
|
|
"\n",
|
|
"to_copy.extend(sloticons)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 42,
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"application/vnd.jupyter.widget-view+json": {
|
|
"model_id": "3497a8d33d6a45879586d4c7441e3f60",
|
|
"version_major": 2,
|
|
"version_minor": 0
|
|
},
|
|
"text/plain": [
|
|
"IntProgress(value=0, max=1288)"
|
|
]
|
|
},
|
|
"metadata": {},
|
|
"output_type": "display_data"
|
|
},
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"1288 of 1288 | 100.00% \n",
|
|
"Done\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"import shutil\n",
|
|
"\n",
|
|
"destpath = Path(\"img/stationpedia\")\n",
|
|
"total_files = len(to_copy)\n",
|
|
"\n",
|
|
"from ipywidgets import IntProgress\n",
|
|
"from IPython.display import display\n",
|
|
"import time\n",
|
|
"\n",
|
|
"f = IntProgress(min=0, max=total_files)\n",
|
|
"display(f)\n",
|
|
"count = 0\n",
|
|
"print ( f\"{count} of {total_files} | { count / total_files * 100}\", end=\"\\r\")\n",
|
|
"for name, file in to_copy:\n",
|
|
" source = datapath / file\n",
|
|
" dest = destpath / f\"{name}.png\"\n",
|
|
" shutil.copy(source, dest)\n",
|
|
" count += 1\n",
|
|
" f.value = count\n",
|
|
" print ( f\"{count} of {total_files} | { (count / total_files) * 100 :.2f}% \", end=\"\\r\")\n",
|
|
"print()\n",
|
|
"print(\"Done\")\n",
|
|
"\n"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.12.5"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 2
|
|
}
|