Feature Complete Shipping V1.0
This commit is contained in:
@@ -6,14 +6,20 @@ import tempfile
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from scanner.scan_ki5_workshop import (
|
||||
MAX_METADATA_CHARS,
|
||||
MAX_SCAN_DECODED_PIXELS,
|
||||
MAX_VARIANT_MASK_SKINS,
|
||||
ModCandidate,
|
||||
PaintCan,
|
||||
ScanFileError,
|
||||
_add_decoded_pixels,
|
||||
analyze_colors,
|
||||
build_variant_mask,
|
||||
discover_paint_cans,
|
||||
discover_mods,
|
||||
main,
|
||||
@@ -168,6 +174,117 @@ class ScannerTests(unittest.TestCase):
|
||||
[(color.paint_can.item_id, color.percentage) for color in colors],
|
||||
)
|
||||
|
||||
def test_dark_chromatic_shading_preserves_the_paint_hue(self) -> None:
|
||||
paints = (
|
||||
PaintCan("Base.PaintBlack", "Paint - Black", (0.20, 0.20, 0.20), 0.1),
|
||||
PaintCan("Base.PaintBlue", "Paint - Blue", (0.35, 0.35, 0.80), 0.1),
|
||||
PaintCan("Base.PaintGreen", "Paint - Green", (0.41, 0.80, 0.41), 0.1),
|
||||
PaintCan("Base.PaintGrey", "Paint - Gray", (0.50, 0.50, 0.50), 0.1),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
image_path = Path(temporary_directory) / "shaded-shell.png"
|
||||
image = Image.new("RGB", (10, 1))
|
||||
image.putdata([(20, 50, 30)] * 8 + [(20, 20, 20)] * 2)
|
||||
image.save(image_path)
|
||||
|
||||
colors = analyze_colors(image_path, paints, preserve_chromatic_hue=True)
|
||||
|
||||
self.assertEqual(
|
||||
[("Base.PaintGreen", 80.0), ("Base.PaintBlack", 20.0)],
|
||||
[(color.paint_can.item_id, color.percentage) for color in colors],
|
||||
)
|
||||
|
||||
def test_dark_chromatic_pixels_remain_conservative_without_a_body_mask(self) -> None:
|
||||
paints = (
|
||||
PaintCan("Base.PaintBlack", "Paint - Black", (0.20, 0.20, 0.20), 0.1),
|
||||
PaintCan("Base.PaintGreen", "Paint - Green", (0.41, 0.80, 0.41), 0.1),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
image_path = Path(temporary_directory) / "dark-tinted-trim.png"
|
||||
Image.new("RGB", (1, 1), (20, 50, 30)).save(image_path)
|
||||
|
||||
colors = analyze_colors(image_path, paints)
|
||||
|
||||
self.assertEqual("Base.PaintBlack", colors[0].paint_can.item_id)
|
||||
|
||||
def test_variant_mask_excludes_fixed_shell_trim_from_paint_percentages(self) -> None:
|
||||
paints = (
|
||||
PaintCan("Base.PaintBlack", "Paint - Black", (0.20, 0.20, 0.20), 0.1),
|
||||
PaintCan("Base.PaintGreen", "Paint - Green", (0.41, 0.80, 0.41), 0.1),
|
||||
PaintCan("Base.PaintRed", "Paint - Red", (0.63, 0.10, 0.10), 0.1),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
green_path = root / "shell-green.png"
|
||||
red_path = root / "shell-red.png"
|
||||
green = Image.new("RGB", (10, 1))
|
||||
green.putdata([(20, 50, 30)] * 8 + [(20, 20, 20)] * 2)
|
||||
green.save(green_path)
|
||||
red = Image.new("RGB", (10, 1))
|
||||
red.putdata([(80, 20, 20)] * 8 + [(20, 20, 20)] * 2)
|
||||
red.save(red_path)
|
||||
|
||||
mask = build_variant_mask((green_path, red_path))
|
||||
colors = analyze_colors(
|
||||
green_path,
|
||||
paints,
|
||||
pixel_mask=mask,
|
||||
preserve_chromatic_hue=True,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[("Base.PaintGreen", 100.0)],
|
||||
[(color.paint_can.item_id, color.percentage) for color in colors],
|
||||
)
|
||||
|
||||
def test_variant_mask_is_not_used_when_sibling_uv_layouts_do_not_overlap(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
left_path = root / "left.png"
|
||||
right_path = root / "right.png"
|
||||
left = Image.new("RGBA", (10, 1), (0, 0, 0, 0))
|
||||
left.putdata([(20, 50, 30, 255)] * 5 + [(0, 0, 0, 0)] * 5)
|
||||
left.save(left_path)
|
||||
right = Image.new("RGBA", (10, 1), (0, 0, 0, 0))
|
||||
right.putdata([(0, 0, 0, 0)] * 5 + [(80, 20, 20, 255)] * 5)
|
||||
right.save(right_path)
|
||||
|
||||
self.assertIsNone(build_variant_mask((left_path, right_path)))
|
||||
|
||||
def test_variant_mask_is_not_used_for_small_livery_differences(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
plain_path = root / "plain.png"
|
||||
decal_path = root / "decal.png"
|
||||
Image.new("RGB", (100, 1), (20, 50, 30)).save(plain_path)
|
||||
decal = Image.new("RGB", (100, 1), (20, 50, 30))
|
||||
decal.putdata([(80, 20, 20)] * 10 + [(20, 50, 30)] * 90)
|
||||
decal.save(decal_path)
|
||||
|
||||
self.assertIsNone(build_variant_mask((plain_path, decal_path)))
|
||||
|
||||
def test_variant_mask_rejects_excessive_skin_count_before_decoding(self) -> None:
|
||||
missing_paths = tuple(
|
||||
Path(f"missing-{index}.png") for index in range(MAX_VARIANT_MASK_SKINS + 1)
|
||||
)
|
||||
|
||||
self.assertIsNone(build_variant_mask(missing_paths))
|
||||
|
||||
def test_scan_rejects_aggregate_decoded_pixel_budget(self) -> None:
|
||||
with self.assertRaisesRegex(ScanFileError, "decoded-pixel safety limit"):
|
||||
_add_decoded_pixels(MAX_SCAN_DECODED_PIXELS - 1, 2)
|
||||
|
||||
def test_png_metadata_value_error_is_normalized(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
texture = Path(temporary_directory) / "metadata.png"
|
||||
texture.write_bytes(b"placeholder")
|
||||
with patch(
|
||||
"scanner.scan_ki5_workshop.Image.open",
|
||||
side_effect=ValueError("compressed metadata is too large"),
|
||||
):
|
||||
with self.assertRaisesRegex(ScanFileError, "Could not decode PNG"):
|
||||
analyze_colors(texture, self.TEST_PAINTS)
|
||||
|
||||
def test_ignores_fully_transparent_pixels(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
image_path = Path(temporary_directory) / "alpha.png"
|
||||
@@ -252,6 +369,53 @@ class ScannerTests(unittest.TestCase):
|
||||
self.assertIn("B42 paint cans: 3", console)
|
||||
self.assertIn("1 mods, 1 cars, 1 textures", console)
|
||||
|
||||
def test_scan_uses_sibling_skins_to_ignore_fixed_shell_trim(self) -> None:
|
||||
paints = (
|
||||
PaintCan("Base.PaintBlack", "Paint - Black", (0.20, 0.20, 0.20), 0.1),
|
||||
PaintCan("Base.PaintGreen", "Paint - Green", (0.41, 0.80, 0.41), 0.1),
|
||||
PaintCan("Base.PaintRed", "Paint - Red", (0.63, 0.10, 0.10), 0.1),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
mod_root = root / "999/mods/shadedCar"
|
||||
self._write(
|
||||
mod_root / "42.13/mod.info",
|
||||
"name=Shaded Car\nid=shadedCar\nrequire=damnlib\ncategory=vehicle\n",
|
||||
)
|
||||
self._write(
|
||||
mod_root / "42.13/media/scripts/vehicles/car.txt",
|
||||
"""
|
||||
module Base {
|
||||
vehicle ShadedCar {
|
||||
engineForce = 4000,
|
||||
skin { texture = Vehicles/ShadedCar_Green, }
|
||||
skin { texture = Vehicles/ShadedCar_Red, }
|
||||
}
|
||||
}
|
||||
""",
|
||||
)
|
||||
texture_root = mod_root / "common/media/textures/Vehicles"
|
||||
texture_root.mkdir(parents=True)
|
||||
green = Image.new("RGB", (10, 1))
|
||||
green.putdata([(20, 50, 30)] * 8 + [(20, 20, 20)] * 2)
|
||||
green.save(texture_root / "ShadedCar_Green.png")
|
||||
red = Image.new("RGB", (10, 1))
|
||||
red.putdata([(80, 20, 20)] * 8 + [(20, 20, 20)] * 2)
|
||||
red.save(texture_root / "ShadedCar_Red.png")
|
||||
|
||||
report = scan_workshop(root, paints)
|
||||
|
||||
self.assertEqual(
|
||||
[
|
||||
[("Base.PaintGreen", 100.0)],
|
||||
[("Base.PaintRed", 100.0)],
|
||||
],
|
||||
[
|
||||
[(paint.paint_can.item_id, paint.percentage) for paint in texture.paints]
|
||||
for texture in report.textures
|
||||
],
|
||||
)
|
||||
|
||||
def test_missing_texture_adds_warning_and_keeps_scanning(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
|
||||
Reference in New Issue
Block a user