Feature Complete Shipping V1.0
This commit is contained in:
@@ -48,6 +48,39 @@ package.loaded["PaintMyKI5/PaintMyKI5VehiclePaintData"] = true
|
||||
|
||||
require "PaintMyKI5/PaintRequirements"
|
||||
require "PaintMyKI5/PaintInventory"
|
||||
require "PaintMyKI5/TextureName"
|
||||
|
||||
test("texture labels use the unique suffix of a shared filename prefix", function()
|
||||
local labels = PaintMyKI5.TextureName.buildLabels({
|
||||
{ texture = "Vehicles/Vehicles_93mustangGT_Shell_Green" },
|
||||
{ texture = "Vehicles/Vehicles_93mustangGT_Shell_Seafoam" },
|
||||
{ texture = "Vehicles/Vehicles_93mustangGT_Shell_SSP" },
|
||||
})
|
||||
assert(labels[1] == "Green")
|
||||
assert(labels[2] == "Seafoam")
|
||||
assert(labels[3] == "SSP")
|
||||
end)
|
||||
|
||||
test("texture labels keep filenames when there is no useful shared prefix", function()
|
||||
local labels = PaintMyKI5.TextureName.buildLabels({
|
||||
{ texture = "Vehicles/Ford_Green.png" },
|
||||
{ texture = "Vehicles/Chevrolet_Red.png" },
|
||||
})
|
||||
assert(labels[1] == "Ford_Green")
|
||||
assert(labels[2] == "Chevrolet_Red")
|
||||
|
||||
local single = PaintMyKI5.TextureName.buildLabels({
|
||||
{ texture = "Vehicles/Only_Skin" },
|
||||
})
|
||||
assert(single[1] == "Only_Skin")
|
||||
|
||||
local duplicates = PaintMyKI5.TextureName.buildLabels({
|
||||
{ texture = "Vehicles/Repeated_Green" },
|
||||
{ texture = "Vehicles/Repeated_Green" },
|
||||
})
|
||||
assert(duplicates[1] == "Repeated_Green")
|
||||
assert(duplicates[2] == "Repeated_Green")
|
||||
end)
|
||||
|
||||
local function fakeVehicle()
|
||||
return {
|
||||
@@ -78,6 +111,17 @@ test("invalid skin is rejected", function()
|
||||
assert(PaintMyKI5.PaintRequirements.getRequirements(fakeVehicle(), 1) == nil)
|
||||
end)
|
||||
|
||||
test("a paint selection is valid before the player reaches the vehicle area", function()
|
||||
local vehicle = fakeVehicle()
|
||||
vehicle.isInArea = function() return false end
|
||||
local character = { getVehicle = function() return nil end }
|
||||
|
||||
assert(PaintMyKI5.PaintRequirements.isValidSelection(character, vehicle, 2))
|
||||
assert(not PaintMyKI5.PaintRequirements.isValidTarget(
|
||||
character, vehicle, 2, "Engine"
|
||||
))
|
||||
end)
|
||||
|
||||
local syncCount = 0
|
||||
local removedCount = 0
|
||||
local addedCount = 0
|
||||
@@ -146,6 +190,35 @@ test("a missing color prevents every mutation", function()
|
||||
assertNear(green.delta, 1.0)
|
||||
end)
|
||||
|
||||
test("painting readiness requires reusable sandpaper and a paintbrush", function()
|
||||
local green = fakeItem("Base.PaintGreen", 1.0)
|
||||
local sandpaper = { marker = "sandpaper" }
|
||||
local craftedBrush = { marker = "crafted brush" }
|
||||
local byType = {
|
||||
["Base.PaintGreen"] = javaList({ green }),
|
||||
}
|
||||
local inventory = {
|
||||
getAllTypeRecurse = function(_, itemType)
|
||||
return byType[itemType] or javaList({})
|
||||
end,
|
||||
}
|
||||
local character = { getInventory = function() return inventory end }
|
||||
local requirements = { { item = "Base.PaintGreen", uses = 1 } }
|
||||
|
||||
local ready, missingPaint, missingTools =
|
||||
PaintMyKI5.PaintInventory.checkPaintingRequirements(character, requirements)
|
||||
assert(not ready and #missingPaint == 0 and #missingTools == 2)
|
||||
assert(missingTools[1].item == "PaintMyKI5.Sandpaper")
|
||||
assert(missingTools[2].item == "Base.Paintbrush")
|
||||
|
||||
byType["PaintMyKI5.Sandpaper"] = javaList({ sandpaper })
|
||||
byType["Base.PaintbrushCrafted"] = javaList({ craftedBrush })
|
||||
ready, missingPaint, missingTools =
|
||||
PaintMyKI5.PaintInventory.checkPaintingRequirements(character, requirements)
|
||||
assert(ready and #missingPaint == 0 and #missingTools == 0)
|
||||
assert(sandpaper.marker == "sandpaper" and craftedBrush.marker == "crafted brush")
|
||||
end)
|
||||
|
||||
test("replacement failure rolls back earlier paint", function()
|
||||
local green = fakeItem("Base.PaintGreen", 1.0)
|
||||
local white = fakeItem("Base.PaintWhite", 0.1, true)
|
||||
@@ -184,6 +257,7 @@ isClient = function() return false end
|
||||
isServer = function() return true end
|
||||
dofile(luaRoot .. "/shared/PaintMyKI5/TimedActions/ISPaintMyKI5Vehicle.lua")
|
||||
PaintMyKI5.PaintInventory.checkRequirements = function() return true end
|
||||
PaintMyKI5.PaintInventory.checkPaintingRequirements = function() return true end
|
||||
|
||||
local function serverCharacter()
|
||||
return {
|
||||
@@ -204,6 +278,23 @@ local function mutableVehicle()
|
||||
return vehicle
|
||||
end
|
||||
|
||||
test("server completion rejects missing tools before changing skin or paint", function()
|
||||
local vehicle = mutableVehicle()
|
||||
local consumed = 0
|
||||
PaintMyKI5.PaintInventory.checkPaintingRequirements = function()
|
||||
return false, {}, { { item = "PaintMyKI5.Sandpaper" } }
|
||||
end
|
||||
PaintMyKI5.PaintInventory.consumeRequirements = function()
|
||||
consumed = consumed + 1
|
||||
return true
|
||||
end
|
||||
local action = ISPaintMyKI5Vehicle:new(serverCharacter(), vehicle, 2, "Engine")
|
||||
action:serverStart()
|
||||
assert(not action:complete())
|
||||
assert(vehicle.skin == 0 and vehicle.transmitted == 0 and consumed == 0)
|
||||
PaintMyKI5.PaintInventory.checkPaintingRequirements = function() return true end
|
||||
end)
|
||||
|
||||
test("server completion consumes then transmits the selected skin", function()
|
||||
local vehicle = mutableVehicle()
|
||||
local character = serverCharacter()
|
||||
@@ -288,4 +379,74 @@ test("client completion never mutates inventory or the vehicle", function()
|
||||
isClient = function() return false end
|
||||
end)
|
||||
|
||||
test("sandpaper is rare wherever full paint cans spawn", function()
|
||||
ProceduralDistributions = {
|
||||
list = {
|
||||
PaintShelf = { items = { "PaintBlack", 10, "Paintbrush", 10 } },
|
||||
PaintJunk = {
|
||||
items = { "Hammer", 1 },
|
||||
junk = { items = { "Base.PaintGreen", 0.4 } },
|
||||
},
|
||||
EmptyBuckets = { items = { "PaintbucketEmpty", 10, "Paintbrush", 10 } },
|
||||
OtherModPaint = { items = { "OtherMod.PaintGreen", 10 } },
|
||||
AlreadyAdded = {
|
||||
items = { "PaintWhite", 1, "PaintMyKI5.Sandpaper", 0.1 },
|
||||
},
|
||||
},
|
||||
}
|
||||
ClutterTables = {
|
||||
ClosetItems = { "PaintPurple", 0.1, "Pillow", 1 },
|
||||
}
|
||||
|
||||
dofile(luaRoot .. "/server/Items/PaintMyKI5_SandpaperDistribution.lua")
|
||||
|
||||
local function countItem(items, itemType)
|
||||
local count = 0
|
||||
local weight = nil
|
||||
for index = 1, #items, 2 do
|
||||
if items[index] == itemType then
|
||||
count = count + 1
|
||||
weight = items[index + 1]
|
||||
end
|
||||
end
|
||||
return count, weight
|
||||
end
|
||||
|
||||
local shelfCount, shelfWeight = countItem(
|
||||
ProceduralDistributions.list.PaintShelf.items,
|
||||
"PaintMyKI5.Sandpaper"
|
||||
)
|
||||
assert(shelfCount == 1 and shelfWeight == 0.1)
|
||||
|
||||
local junkCount, junkWeight = countItem(
|
||||
ProceduralDistributions.list.PaintJunk.junk.items,
|
||||
"PaintMyKI5.Sandpaper"
|
||||
)
|
||||
assert(junkCount == 1 and junkWeight == 0.1)
|
||||
|
||||
local emptyCount = countItem(
|
||||
ProceduralDistributions.list.EmptyBuckets.items,
|
||||
"PaintMyKI5.Sandpaper"
|
||||
)
|
||||
assert(emptyCount == 0)
|
||||
|
||||
local otherModCount = countItem(
|
||||
ProceduralDistributions.list.OtherModPaint.items,
|
||||
"PaintMyKI5.Sandpaper"
|
||||
)
|
||||
assert(otherModCount == 0)
|
||||
|
||||
local existingCount = countItem(
|
||||
ProceduralDistributions.list.AlreadyAdded.items,
|
||||
"PaintMyKI5.Sandpaper"
|
||||
)
|
||||
assert(existingCount == 1)
|
||||
|
||||
local clutterCount, clutterWeight = countItem(
|
||||
ClutterTables.ClosetItems,
|
||||
"PaintMyKI5.Sandpaper"
|
||||
)
|
||||
assert(clutterCount == 1 and clutterWeight == 0.1)
|
||||
end)
|
||||
|
||||
print("Lua tests passed: " .. passed)
|
||||
|
||||
@@ -66,11 +66,66 @@ class InGameLuaContractTests(unittest.TestCase):
|
||||
"shared/PaintMyKI5/TimedActions/ISPaintMyKI5Vehicle.lua",
|
||||
"client/PaintMyKI5/ISPaintMyKI5UI.lua",
|
||||
"client/PaintMyKI5/PaintVehicleContextMenu.lua",
|
||||
"shared/PaintMyKI5/TextureName.lua",
|
||||
)
|
||||
|
||||
def test_runtime_files_exist(self) -> None:
|
||||
self.assertEqual([], [name for name in self.EXPECTED_FILES if not (LUA_ROOT / name).is_file()])
|
||||
|
||||
def test_art_assets_are_packaged_for_mod_and_context_menus(self) -> None:
|
||||
source_icon = (PROJECT_ROOT / "art/icon.png").read_bytes()
|
||||
source_preview = (PROJECT_ROOT / "art/preview.png").read_bytes()
|
||||
|
||||
for directory in (PROJECT_ROOT, PROJECT_ROOT / "42.20"):
|
||||
self.assertEqual(source_icon, (directory / "icon.png").read_bytes())
|
||||
self.assertEqual(source_preview, (directory / "preview.png").read_bytes())
|
||||
metadata = (directory / "mod.info").read_text(encoding="utf-8")
|
||||
self.assertIn("icon=icon.png", metadata)
|
||||
self.assertIn("poster=preview.png", metadata)
|
||||
|
||||
self.assertEqual(
|
||||
source_icon,
|
||||
(PROJECT_ROOT / "common/media/textures/PaintMyKI5_ContextMenu.png").read_bytes(),
|
||||
)
|
||||
context_source = (LUA_ROOT / self.EXPECTED_FILES[4]).read_text(encoding="utf-8")
|
||||
self.assertIn(
|
||||
'getTexture("media/textures/PaintMyKI5_ContextMenu.png")',
|
||||
context_source,
|
||||
)
|
||||
|
||||
def test_sandpaper_item_icon_translation_and_loot_loader_are_packaged(self) -> None:
|
||||
item_script = (
|
||||
PROJECT_ROOT / "42.20/media/scripts/PaintMyKI5_items.txt"
|
||||
).read_text(encoding="utf-8")
|
||||
sandpaper = re.search(r"item\s+Sandpaper\s*\{(.*?)\}", item_script, re.S)
|
||||
self.assertIsNotNone(sandpaper)
|
||||
self.assertIn("ItemType = base:normal", sandpaper.group(1))
|
||||
self.assertIn("Icon = Sandpaper", sandpaper.group(1))
|
||||
self.assertEqual(
|
||||
(PROJECT_ROOT / "art/sandpaper.png").read_bytes(),
|
||||
(PROJECT_ROOT / "common/media/textures/Item_Sandpaper.png").read_bytes(),
|
||||
)
|
||||
|
||||
translations = json.loads(
|
||||
(
|
||||
PROJECT_ROOT
|
||||
/ "42.20/media/lua/shared/Translate/EN/ItemName.json"
|
||||
).read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual("Sandpaper", translations["ItemName_PaintMyKI5.Sandpaper"])
|
||||
self.assertFalse(
|
||||
(
|
||||
PROJECT_ROOT
|
||||
/ "42.20/media/lua/shared/Translate/EN/ItemName_EN.json"
|
||||
).exists()
|
||||
)
|
||||
self.assertTrue(
|
||||
(
|
||||
PROJECT_ROOT
|
||||
/ "42.20/media/lua/server/Items/PaintMyKI5_SandpaperDistribution.lua"
|
||||
).is_file()
|
||||
)
|
||||
|
||||
def test_server_authoritative_timed_action_contract(self) -> None:
|
||||
source = (LUA_ROOT / self.EXPECTED_FILES[2]).read_text(encoding="utf-8")
|
||||
|
||||
@@ -78,6 +133,7 @@ class InGameLuaContractTests(unittest.TestCase):
|
||||
self.assertIn("function ISPaintMyKI5Vehicle:complete()", source)
|
||||
self.assertIn("PaintRequirements.getRequirements", source)
|
||||
self.assertIn("PaintInventory.consumeRequirements", source)
|
||||
self.assertGreaterEqual(source.count("checkPaintingRequirements"), 2)
|
||||
self.assertIn("vehicle:setSkinIndex", source)
|
||||
self.assertIn("vehicle:transmitSkinIndex", source)
|
||||
self.assertNotRegex(source, r"sendClientCommand\([^\n]+setSkinIndex")
|
||||
@@ -91,8 +147,26 @@ class InGameLuaContractTests(unittest.TestCase):
|
||||
self.assertIn("getTexture", ui_source)
|
||||
self.assertIn("or getTexture(textureData.texture)", ui_source)
|
||||
self.assertIn("drawTextureScaledAspect", ui_source)
|
||||
self.assertGreaterEqual(ui_source.count("PaintRequirements.isValidTarget"), 2)
|
||||
self.assertIn("PaintMyKI5.TextureName.buildLabels", ui_source)
|
||||
self.assertIn("paintButton:setTooltip", ui_source)
|
||||
self.assertIn("checkPaintingRequirements", ui_source)
|
||||
self.assertGreaterEqual(ui_source.count("PaintRequirements.isValidSelection"), 2)
|
||||
self.assertNotIn("PaintRequirements.isValidTarget", ui_source)
|
||||
self.assertIn("ISTimedActionQueue.add", ui_source)
|
||||
self.assertLess(
|
||||
ui_source.index("ISPathFindAction:pathToVehicleArea"),
|
||||
ui_source.index("ISPaintMyKI5Vehicle:new"),
|
||||
)
|
||||
|
||||
translations = json.loads(
|
||||
(LUA_ROOT / "shared/Translate/EN/IG_UI.json").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(
|
||||
"Missing requirements:",
|
||||
translations["IGUI_PaintMyKI5_MissingRequirements"],
|
||||
)
|
||||
self.assertIn("%1", translations["IGUI_PaintMyKI5_MissingTool"])
|
||||
self.assertIn("%1", translations["IGUI_PaintMyKI5_MissingPaint"])
|
||||
|
||||
def test_b42_translation_files_are_valid_json(self) -> None:
|
||||
context = json.loads(
|
||||
@@ -121,6 +195,31 @@ class InGameLuaContractTests(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_generated_mustang_green_skin_is_classified_as_green(self) -> None:
|
||||
manifest = (
|
||||
LUA_ROOT / "shared/PaintMyKI5/PaintMyKI5VehiclePaintData.lua"
|
||||
).read_text(encoding="utf-8")
|
||||
vehicle = re.search(
|
||||
r'\["Base\.93mustangGT"\] = \{(.*?)\n \},\n'
|
||||
r' \["Base\.93mustangSSP"\]',
|
||||
manifest,
|
||||
re.S,
|
||||
)
|
||||
self.assertIsNotNone(vehicle)
|
||||
green_skin = re.search(
|
||||
r'texture = "Vehicles/Vehicles_93mustangGT_Shell_Green",'
|
||||
r"(.*?)\n \},",
|
||||
vehicle.group(1),
|
||||
re.S,
|
||||
)
|
||||
self.assertIsNotNone(green_skin)
|
||||
green_percentage = re.search(
|
||||
r'item = "Base\.PaintGreen", percent = ([\d.]+)',
|
||||
green_skin.group(1),
|
||||
)
|
||||
self.assertIsNotNone(green_percentage)
|
||||
self.assertGreaterEqual(float(green_percentage.group(1)), 90.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -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