Files
nevimer 00ccf0c6b5 Merge remote-tracking branch 'tgstation/master' into upstream-feb12-2026
# Conflicts:
#	.github/CODEOWNERS
#	.github/workflows/compile_changelogs.yml
#	.github/workflows/stale.yml
#	SQL/database_changelog.md
#	_maps/map_files/CatwalkStation/CatwalkStation_2023.dmm
#	code/__DEFINES/atom_hud.dm
#	code/__DEFINES/inventory.dm
#	code/__DEFINES/mobs.dm
#	code/__DEFINES/species_clothing_paths.dm
#	code/__DEFINES/subsystems.dm
#	code/__DEFINES/surgery.dm
#	code/__HELPERS/global_lists.dm
#	code/_globalvars/lists/maintenance_loot.dm
#	code/_globalvars/traits/_traits.dm
#	code/controllers/subsystem/minor_mapping.dm
#	code/controllers/subsystem/processing/quirks.dm
#	code/controllers/subsystem/shuttle.dm
#	code/datums/components/palette.dm
#	code/datums/components/surgery_initiator.dm
#	code/datums/diseases/advance/advance.dm
#	code/datums/hud.dm
#	code/datums/mood.dm
#	code/datums/mutations/chameleon.dm
#	code/datums/quirks/negative_quirks/nyctophobia.dm
#	code/datums/status_effects/debuffs/debuffs.dm
#	code/datums/status_effects/debuffs/drunk.dm
#	code/datums/status_effects/debuffs/slime/slime_leech.dm
#	code/datums/weather/weather.dm
#	code/game/data_huds.dm
#	code/game/objects/items.dm
#	code/game/objects/items/devices/scanners/health_analyzer.dm
#	code/game/objects/items/frog_statue.dm
#	code/game/objects/items/rcd/RLD.dm
#	code/game/objects/items/robot/items/hypo.dm
#	code/game/objects/items/stacks/medical.dm
#	code/game/objects/items/stacks/wrap.dm
#	code/game/objects/items/storage/garment.dm
#	code/game/objects/items/tools/medical/defib.dm
#	code/game/objects/items/weaponry.dm
#	code/game/objects/items/weaponry/melee/misc.dm
#	code/game/objects/structures/crates_lockers/closets/secure/security.dm
#	code/game/objects/structures/curtains.dm
#	code/game/objects/structures/dresser.dm
#	code/game/objects/structures/girders.dm
#	code/game/objects/structures/maintenance.dm
#	code/game/objects/structures/mirror.dm
#	code/modules/admin/greyscale_modify_menu.dm
#	code/modules/admin/verbs/light_debug.dm
#	code/modules/antagonists/ashwalker/ashwalker.dm
#	code/modules/antagonists/heretic/knowledge/starting_lore.dm
#	code/modules/antagonists/ninja/ninjaDrainAct.dm
#	code/modules/art/paintings.dm
#	code/modules/client/preferences.dm
#	code/modules/client/verbs/ooc.dm
#	code/modules/clothing/head/wig.dm
#	code/modules/events/disease_outbreak.dm
#	code/modules/holodeck/holo_effect.dm
#	code/modules/jobs/job_types/head_of_security.dm
#	code/modules/jobs/job_types/security_officer.dm
#	code/modules/library/skill_learning/generic_skillchips/point.dm
#	code/modules/mining/lavaland/ash_flora.dm
#	code/modules/mining/lavaland/mining_loot/megafauna/ash_drake.dm
#	code/modules/mob/dead/new_player/new_player.dm
#	code/modules/mob/living/basic/guardian/guardian.dm
#	code/modules/mob/living/basic/space_fauna/space_dragon/space_dragon.dm
#	code/modules/mob/living/carbon/carbon.dm
#	code/modules/mob/living/carbon/human/human.dm
#	code/modules/mob/living/carbon/human/human_defines.dm
#	code/modules/mob/living/carbon/life.dm
#	code/modules/mob/living/living.dm
#	code/modules/mob/living/living_defines.dm
#	code/modules/mob/mob.dm
#	code/modules/mob_spawn/ghost_roles/mining_roles.dm
#	code/modules/mod/mod_control.dm
#	code/modules/mod/modules/modules_general.dm
#	code/modules/modular_computers/computers/item/computer_ui.dm
#	code/modules/paperwork/paper.dm
#	code/modules/paperwork/paperbin.dm
#	code/modules/power/lighting/light.dm
#	code/modules/projectiles/guns/energy/kinetic_accelerator.dm
#	code/modules/projectiles/projectile.dm
#	code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm
#	code/modules/reagents/chemistry/reagents/food_reagents.dm
#	code/modules/reagents/chemistry/reagents/other_reagents.dm
#	code/modules/reagents/chemistry/reagents/pyrotechnic_reagents.dm
#	code/modules/research/xenobiology/crossbreeding/_clothing.dm
#	code/modules/research/xenobiology/crossbreeding/prismatic.dm
#	code/modules/surgery/advanced/brainwashing.dm
#	code/modules/surgery/advanced/lobotomy.dm
#	code/modules/surgery/amputation.dm
#	code/modules/surgery/blood_filter.dm
#	code/modules/surgery/bodyparts/_bodyparts.dm
#	code/modules/surgery/brain_surgery.dm
#	code/modules/surgery/cavity_implant.dm
#	code/modules/surgery/coronary_bypass.dm
#	code/modules/surgery/gastrectomy.dm
#	code/modules/surgery/healing.dm
#	code/modules/surgery/limb_augmentation.dm
#	code/modules/surgery/organ_manipulation.dm
#	code/modules/surgery/revival.dm
#	code/modules/surgery/sleeper_protocol.dm
#	code/modules/surgery/surgery_helpers.dm
#	code/modules/surgery/surgery_step.dm
#	code/modules/unit_tests/_unit_tests.dm
#	code/modules/unit_tests/designs.dm
#	code/modules/unit_tests/icon_state_worn.dm
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_cultist.png
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_headrevolutionary.png
#	code/modules/unit_tests/screenshots/screenshot_antag_icons_provocateur.png
#	code/modules/unit_tests/screenshots/screenshot_husk_body.png
#	code/modules/unit_tests/screenshots/screenshot_husk_body_missing_limbs.png
#	icons/map_icons/clothing/head/_head.dmi
#	icons/map_icons/clothing/shoes.dmi
#	icons/map_icons/items/_item.dmi
#	icons/mob/huds/hud.dmi
#	icons/mob/inhands/64x64_lefthand.dmi
#	icons/mob/inhands/64x64_righthand.dmi
#	icons/obj/machines/computer.dmi
#	tgui/packages/tgui/interfaces/OperatingComputer.jsx
#	tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/MainPage.tsx
#	tgui/packages/tgui/interfaces/PreferencesMenu/types.ts
#	tgui/packages/tgui/interfaces/SurgeryInitiator.tsx
#	tools/icon_cutter/check.py
2026-02-12 23:50:09 -05:00

141 lines
5.0 KiB
Python

import glob
import os
import re
import sys
import hashlib
import platform
import subprocess
from PIL import Image
import numpy as np
import difflib
BUF_SIZE = 65536 # lets read stuff in 64kb chunks!
chop_filename = re.compile(r"^.*(\..+?)$", re.M)
chop_extension = re.compile(r"^(.*)\..+?$", re.M)
def reshape_split(image: np.ndarray, cut_to: tuple):
img_height, img_width, channels = image.shape
tile_height, tile_width = cut_to
tiled_array = image.reshape(img_height // tile_height,
tile_height,
img_width // tile_width,
tile_width,
channels)
tiled_array = tiled_array.swapaxes(1, 2)
tiled_array = tiled_array.reshape(tile_height, -1, channels)
return tiled_array
def get_file_hash(path):
path_suffix = re.sub(chop_filename, r"\1", path, count = 1)
if path_suffix == ".dmi" or path_suffix == ".png":
return hash_dmi(path)
else:
return hash_file(path)
def hash_dmi(path):
md5 = hashlib.md5()
dmi = Image.open(path)
dmi = dmi.convert('RGBA')
dmi.load() # Needed only for .png EXIF data (see citation above)
dmi_metadata = dmi.info['Description']
md5.update(dmi_metadata.encode('utf-8'))
readable_metadata = dict(
map(lambda entry: (entry[0], entry[1]),
map(lambda entry : (entry[0].strip(), entry[1].strip()),
filter(lambda entry: entry[0].strip() == 'width' or entry[0].strip() == 'height',
map(lambda entry : entry.split("="), dmi_metadata.split("\n"))))))
icon_hash = hashlib.md5()
divided_dmi = reshape_split(np.asarray(dmi), (int(readable_metadata['height']), int(readable_metadata['width'])))
for i in range(divided_dmi.shape[0]):
bytes = divided_dmi[1].tobytes()
md5.update(bytes)
icon_hash.update(bytes)
return (md5.hexdigest(), dmi_metadata, icon_hash.hexdigest())
def hash_file(path):
md5 = hashlib.md5()
with open(path, 'rb') as f:
while True:
data = f.read(BUF_SIZE)
if not data:
break
md5.update(data)
return (md5.hexdigest(), None, None)
path_to_us = os.path.realpath(os.path.dirname(__file__))
pass_count = 0
fail_count = 0
output_hash = {}
files = []
if platform.system() == "Windows":
files = glob.glob(f"{path_to_us}\\..\\..\\icons\\**\\*.toml", recursive = True)
files += glob.glob(f"{path_to_us}\..\\..\\modular_zubbers\\icons\\**\*.toml", recursive = True) # BUBBER EDIT ADDITION: Modular icon cutter
else:
files = glob.glob(f"{path_to_us}/../../icons/**/*.toml", recursive = True)
files += glob.glob(f"{path_to_us}\../../modular_zubbers/icons/**\*.toml", recursive = True) # BUBBER EDIT ADDITION: Modular icon cutter
for cutter_template in files:
resource_name = re.sub(chop_extension, r"\1", cutter_template, count = 1)
if not os.path.isfile(resource_name):
print(f"::error template={cutter_template} exists but lacks a matching resource file ({resource_name})")
fail_count += 1
continue
output_name = re.sub(chop_extension, r"\1.dmi", resource_name, count = 1)
if not os.path.isfile(output_name):
print(f"::error template={cutter_template} and resource={resource_name} exist but they lack a matching output={output_name}. (Try rebuilding)")
fail_count += 1
continue
output_hash[output_name] = get_file_hash(output_name)
# Sanity check
if len(output_hash) == 0:
print(f"::error output_hash dict was empty. Something has gone wrong")
sys.exit(1)
# Execute cutter
if platform.system() == "Windows":
subprocess.run(f"{path_to_us}\\..\\build\\build.bat --force-recut --ci icon-cutter")
else:
subprocess.run(f"{path_to_us}/../build/build.sh --force-recut --ci icon-cutter", shell = True)
for output_name in output_hash:
old_hash, old_metadata, old_icon_hash = output_hash[output_name]
new_hash, new_metadata, new_icon_hash = get_file_hash(output_name)
if old_hash == new_hash:
pass_count += 1
continue
if old_metadata != new_metadata:
print("Metadata differs!")
events = ""
current_op = None
working = ""
for index, op in enumerate(difflib.ndiff(old_metadata, new_metadata)):
in_nothing = False
if current_op == None:
current_op = op[0]
if current_op != op[0]:
events += f"{current_op*10}\n{working}\n"
current_op = op[0]
working = ""
if op[0]== ' ':
continue
working += f"{op[-1]}"
events += f"{current_op*10}\n{working}\n"
print(events, end="")
if old_icon_hash != new_icon_hash:
print("Icon hashes differ!")
fail_count += 1
print(f"::error output={output_name} and its templates all exist but were not comitted fully compiled")
print(f"{len(output_hash)} templates checked, {pass_count} passed, {fail_count} failed", end="")
if fail_count > 0:
sys.exit(1)