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", ) 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_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.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.assertGreaterEqual(ui_source.count("PaintRequirements.isValidTarget"), 2) self.assertIn("ISTimedActionQueue.add", ui_source) 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 ) ) if __name__ == "__main__": unittest.main()