from __future__ import annotations import json import re import tempfile import unittest from pathlib import Path from scanner import scan_ki5_workshop as scanner PROJECT_ROOT = Path(__file__).resolve().parents[2] LUA_ROOT = PROJECT_ROOT / "42.20/media/lua" class PaintUseAllocationTests(unittest.TestCase): def test_allocates_one_full_bucket_proportionally(self) -> None: paints = ( scanner.PaintPercentage(scanner.PaintCan("Base.PaintGrey", "Grey", (0.5,) * 3, 0.1), 60.0), scanner.PaintPercentage(scanner.PaintCan("Base.PaintGreen", "Green", (0.4,) * 3, 0.1), 20.0), scanner.PaintPercentage(scanner.PaintCan("Base.PaintWhite", "White", (0.9,) * 3, 0.1), 20.0), ) allocated = scanner.allocate_paint_uses(paints, 10) self.assertEqual( [("Base.PaintGrey", 6.0), ("Base.PaintGreen", 2.0), ("Base.PaintWhite", 2.0)], [(entry.paint_can.item_id, uses) for entry, uses in allocated], ) self.assertAlmostEqual(10.0, sum(uses for _entry, uses in allocated)) def test_equal_remainders_are_deterministic(self) -> None: paints = tuple( scanner.PaintPercentage( scanner.PaintCan(item_id, item_id, (0.5,) * 3, 0.1), percentage, ) for item_id, percentage in ( ("Base.PaintRed", 33.3), ("Base.PaintBlue", 33.3), ("Base.PaintGreen", 33.4), ) ) allocated = scanner.allocate_paint_uses(paints, 10) self.assertEqual( [("Base.PaintGreen", 3.34), ("Base.PaintBlue", 3.33), ("Base.PaintRed", 3.33)], [(entry.paint_can.item_id, uses) for entry, uses in allocated], ) self.assertAlmostEqual(10.0, sum(uses for _entry, uses in allocated)) def test_bucket_capacity_comes_from_game_use_delta(self) -> None: paints = ( scanner.PaintCan("Base.PaintGreen", "Green", (0.4,) * 3, 0.1), scanner.PaintCan("Base.PaintWhite", "White", (0.9,) * 3, 0.1), ) self.assertEqual(10, scanner.paint_bucket_uses(paints)) class InGameLuaContractTests(unittest.TestCase): EXPECTED_FILES = ( "shared/PaintMyKI5/PaintRequirements.lua", "shared/PaintMyKI5/PaintInventory.lua", "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") self.assertIn("ISBaseTimedAction:derive", source) 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") def test_context_menu_and_texture_preview_contract(self) -> None: context_source = (LUA_ROOT / self.EXPECTED_FILES[4]).read_text(encoding="utf-8") ui_source = (LUA_ROOT / self.EXPECTED_FILES[3]).read_text(encoding="utf-8") self.assertIn("Events.OnFillWorldObjectContextMenu.Add", context_source) self.assertIn("getFullName()", context_source) self.assertIn("getTexture", ui_source) self.assertIn("or getTexture(textureData.texture)", ui_source) self.assertIn("drawTextureScaledAspect", ui_source) 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( (LUA_ROOT / "shared/Translate/EN/ContextMenu.json").read_text(encoding="utf-8") ) ui = json.loads((LUA_ROOT / "shared/Translate/EN/IG_UI.json").read_text(encoding="utf-8")) self.assertEqual("Paint vehicle", context["ContextMenu_PaintMyKI5"]) self.assertIn("IGUI_PaintMyKI5_Title", ui) self.assertIn("IGUI_PaintMyKI5_OneBucket", ui) def test_generated_manifest_contains_exact_use_requirements(self) -> None: manifest = ( LUA_ROOT / "shared/PaintMyKI5/PaintMyKI5VehiclePaintData.lua" ).read_text(encoding="utf-8") self.assertIn("schemaVersion = 2", manifest) self.assertIn("bucketUses = 10", manifest) self.assertRegex(manifest, r'item = "Base\.Paint\w+", percent = \d+\.\d, uses = \d+\.\d\d') use_groups = re.findall(r"paints = \{(.*?)\n \},", manifest, re.S) self.assertTrue(use_groups) self.assertTrue( all( abs(sum(map(float, re.findall(r"uses = (\d+\.\d+)", group))) - 10.0) < 0.001 for group in use_groups ) ) 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()