453 lines
16 KiB
Lua
453 lines
16 KiB
Lua
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"
|
|
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 {
|
|
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)
|
|
|
|
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
|
|
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("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)
|
|
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
|
|
PaintMyKI5.PaintInventory.checkPaintingRequirements = 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 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()
|
|
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)
|
|
|
|
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)
|