local luaRoot = PROJECT_ROOT .. "/42.20/media/lua" package.path = table.concat({ luaRoot .. "/shared/?.lua", luaRoot .. "/shared/?/?.lua", package.path, }, ";") local passed = 0 local function test(name, body) local ok, errorMessage = pcall(body) if not ok then error(name .. ": " .. tostring(errorMessage), 0) end passed = passed + 1 end local function assertNear(actual, expected) assert(math.abs(actual - expected) < 0.0001, "expected " .. tostring(expected) .. ", got " .. tostring(actual)) end local function javaList(values) return { size = function() return #values end, get = function(_, index) return values[index + 1] end, } end PaintMyKI5 = { VehiclePaintData = { bucketUses = 10, paintCans = {}, vehicles = { ["Base.TestCar"] = { textures = { { skinIndex = 2, texture = "Vehicles/TestCar_Green", paints = { { item = "Base.PaintGreen", percent = 97.9, uses = 9.79 }, { item = "Base.PaintWhite", percent = 2.1, uses = 0.21 }, }, }, }, }, }, }, } package.loaded["PaintMyKI5/PaintMyKI5VehiclePaintData"] = true require "PaintMyKI5/PaintRequirements" require "PaintMyKI5/PaintInventory" local function fakeVehicle() return { getScript = function() return { getFullName = function() return "Base.TestCar" end, getAreaById = function() return true end, } end, getSkinCount = function() return 3 end, getSkinIndex = function() return 0 end, getCurrentSpeedKmHour = function() return 0 end, isRemovedFromWorld = function() return false end, isInArea = function() return true end, getUseablePart = function() return nil end, } end test("requirements retain fractional proportional uses", function() local requirements = PaintMyKI5.PaintRequirements.getRequirements(fakeVehicle(), 2) assert(#requirements == 2) assertNear(requirements[1].uses, 9.79) assertNear(requirements[2].uses, 0.21) assertNear(requirements[1].uses + requirements[2].uses, 10) end) test("invalid skin is rejected", function() assert(PaintMyKI5.PaintRequirements.getRequirements(fakeVehicle(), 1) == nil) end) local syncCount = 0 local removedCount = 0 local addedCount = 0 sendItemStats = function() syncCount = syncCount + 1 end sendRemoveItemFromContainer = function() removedCount = removedCount + 1 end sendAddItemToContainer = function() addedCount = addedCount + 1 end instanceItem = function() return {} end local function fakeItem(itemType, delta, failAdd) local item = { itemType = itemType, delta = delta, inContainer = true } local container = { DoRemoveItem = function(_, target) target.inContainer = false end, AddItem = function(_, target) if failAdd then return nil end target.inContainer = true return target end, } item.getUseDelta = function() return 0.1 end item.getCurrentUsesFloat = function(self) return self.delta end item.setUsedDelta = function(self, value) self.delta = value end item.getContainer = function() return container end return item end test("inventory aggregates partial cans and consumes exact fractions", function() local greenA = fakeItem("Base.PaintGreen", 0.5) local greenB = fakeItem("Base.PaintGreen", 0.5) local white = fakeItem("Base.PaintWhite", 1.0) local byType = { ["Base.PaintGreen"] = javaList({ greenA, greenB }), ["Base.PaintWhite"] = javaList({ white }), } local character = { getInventory = function() return { getAllTypeRecurse = function(_, itemType) return byType[itemType] end } end, removeFromHands = function() end, } local requirements = { { item = "Base.PaintGreen", uses = 9.79 }, { item = "Base.PaintWhite", uses = 0.21 }, } assert(PaintMyKI5.PaintInventory.consumeRequirements(character, requirements)) assertNear(greenB.delta, 0.021) assertNear(white.delta, 0.979) assert(removedCount == 1 and addedCount == 1) end) test("a missing color prevents every mutation", function() local green = fakeItem("Base.PaintGreen", 1.0) local byType = { ["Base.PaintGreen"] = javaList({ green }), ["Base.PaintWhite"] = javaList({}), } local character = { getInventory = function() return { getAllTypeRecurse = function(_, itemType) return byType[itemType] end } end, removeFromHands = function() end, } assert(not PaintMyKI5.PaintInventory.consumeRequirements(character, { { item = "Base.PaintGreen", uses = 5 }, { item = "Base.PaintWhite", uses = 1 }, })) assertNear(green.delta, 1.0) end) test("replacement failure rolls back earlier paint", function() local green = fakeItem("Base.PaintGreen", 1.0) local white = fakeItem("Base.PaintWhite", 0.1, true) local byType = { ["Base.PaintGreen"] = javaList({ green }), ["Base.PaintWhite"] = javaList({ white }), } local character = { getInventory = function() return { getAllTypeRecurse = function(_, itemType) return byType[itemType] end } end, removeFromHands = function() end, } assert(not PaintMyKI5.PaintInventory.consumeRequirements(character, { { item = "Base.PaintGreen", uses = 1 }, { item = "Base.PaintWhite", uses = 1 }, })) assertNear(green.delta, 1.0) assert(white.inContainer) end) ISBaseTimedAction = { derive = function(_, name) local derived = { Type = name } derived.__index = derived return setmetatable(derived, { __index = ISBaseTimedAction }) end, new = function(actionClass, character) return setmetatable({ character = character }, actionClass) end, stop = function() end, perform = function() end, } package.loaded["TimedActions/ISBaseTimedAction"] = true isClient = function() return false end isServer = function() return true end dofile(luaRoot .. "/shared/PaintMyKI5/TimedActions/ISPaintMyKI5Vehicle.lua") PaintMyKI5.PaintInventory.checkRequirements = function() return true end local function serverCharacter() return { getVehicle = function() return nil end, isTimedActionInstant = function() return false end, } end local function mutableVehicle() local vehicle = fakeVehicle() vehicle.skin = 0 vehicle.transmitted = 0 vehicle.updated = 0 vehicle.getSkinIndex = function(self) return self.skin end vehicle.setSkinIndex = function(self, skin) self.skin = skin end vehicle.transmitSkinIndex = function(self) self.transmitted = self.transmitted + 1 end vehicle.updateSkin = function(self) self.updated = self.updated + 1 end return vehicle end test("server completion consumes then transmits the selected skin", function() local vehicle = mutableVehicle() local character = serverCharacter() local consumed = 0 PaintMyKI5.PaintInventory.consumeRequirements = function() consumed = consumed + 1 return true end local action = ISPaintMyKI5Vehicle:new(character, vehicle, 2, "Engine") action:serverStart() assert(action:complete()) assert(consumed == 1 and vehicle.skin == 2 and vehicle.transmitted == 1) end) test("server rejects forged and raced skin changes without consuming", function() local character = serverCharacter() local consumed = 0 PaintMyKI5.PaintInventory.consumeRequirements = function() consumed = consumed + 1 return true end local forgedVehicle = mutableVehicle() local forged = ISPaintMyKI5Vehicle:new(character, forgedVehicle, 99, "Engine") forged:serverStart() assert(not forged:complete()) local racedVehicle = mutableVehicle() local raced = ISPaintMyKI5Vehicle:new(character, racedVehicle, 2, "Engine") raced:serverStart() racedVehicle.skin = 1 assert(not raced:complete()) assert(consumed == 0) end) test("failed skin or inventory commit restores state without losing paint", function() local character = serverCharacter() local consumed = 0 PaintMyKI5.PaintInventory.consumeRequirements = function() consumed = consumed + 1 return false end local vehicle = mutableVehicle() local action = ISPaintMyKI5Vehicle:new(character, vehicle, 2, "Engine") action:serverStart() assert(not action:complete()) assert(vehicle.skin == 0 and vehicle.transmitted == 2 and consumed == 1) local broken = mutableVehicle() broken.setSkinIndex = function(self, skin) if skin == 2 then error("skin mutation failed") end self.skin = skin end consumed = 0 local brokenAction = ISPaintMyKI5Vehicle:new(character, broken, 2, "Engine") brokenAction:serverStart() assert(not brokenAction:complete()) assert(broken.skin == 0 and consumed == 0) end) test("singleplayer completion updates the local skin", function() isServer = function() return false end local vehicle = mutableVehicle() PaintMyKI5.PaintInventory.consumeRequirements = function() return true end local action = ISPaintMyKI5Vehicle:new(serverCharacter(), vehicle, 2, "Engine") action:serverStart() assert(action:complete()) assert(vehicle.skin == 2 and vehicle.updated == 1 and vehicle.transmitted == 0) isServer = function() return true end end) test("client completion never mutates inventory or the vehicle", function() isClient = function() return true end local vehicle = mutableVehicle() local consumed = 0 PaintMyKI5.PaintInventory.consumeRequirements = function() consumed = consumed + 1 return true end local action = ISPaintMyKI5Vehicle:new(serverCharacter(), vehicle, 1, "Engine") assert(action:complete()) assert(consumed == 0 and vehicle.skin == 0 and vehicle.transmitted == 0) isClient = function() return false end end) print("Lua tests passed: " .. passed)