Created
December 29, 2025 22:14
-
-
Save PhiBabin/947485050b3036fd6dbdd7bcb1d5d8cd to your computer and use it in GitHub Desktop.
Revisions
-
PhiBabin created this gist
Dec 29, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,939 @@ local gadget = gadget ---@type Gadget function gadget:GetInfo() return { name = "AutoColorPicker", desc = "Automatically assigns colors to teams", author = "Damgam, Born2Crawl (color palette), kafka42", date = "2021", license = "GNU GPL, v2 or later", layer = -100, enabled = true, } end local math_pow = math.pow local anonymousMode = Spring.GetModOptions().teamcolors_anonymous_mode local gaiaTeamID = Spring.GetGaiaTeamID() local teamList = Spring.GetTeamList() local allyTeamList = Spring.GetAllyTeamList() local allyTeamCount = #allyTeamList - 1 local isSurvival = Spring.Utilities.Gametype.IsPvE() local survivalColorNum = 1 -- Starting from color #1 local survivalColorVariation = 0 -- Current color variation local allyTeamNum = 0 local teamSizes = {} local myAllyTeamID, myTeamID if not gadgetHandler:IsSyncedCode() then myAllyTeamID = Spring.GetMyAllyTeamID() myTeamID = Spring.GetMyTeamID() end -- Special colors local armBlueColor = "#004DFF" -- Armada Blue local corRedColor = "#FF1005" -- Cortex Red local scavPurpColor = "#6809A1" -- Scav Purple local raptorOrangeColor = "#CC8914" -- Raptor Orange local gaiaGrayColor = "#7F7F7F" -- Gaia Grey local legGreenColor = "#0CE818" -- Legion Green -- NEW IceXuick Colors V6 local ffaColors = { "#004DFF", -- 1 "#FF1005", -- 2 "#0CE908", -- 3 "#FFD200", -- 4 "#F80889", -- 5 "#09F5F5", -- 6 "#FF6107", -- 7 "#F190B3", -- 8 "#097E1C", -- 9 "#C88B2F", -- 10 "#7CA1FF", -- 11 "#9F0D05", -- 12 "#3EFFA2", -- 13 "#F5A200", -- 14 "#C4A9FF", -- 15 "#0B849B", -- 16 "#B4FF39", -- 17 "#FF68EA", -- 18 "#D8EEFF", -- 19 "#689E3D", -- 20 "#B04523", -- 21 "#FFBB7C", -- 22 "#3475FF", -- 23 "#DD783F", -- 24 "#FFAAF3", -- 25 "#4A4376", -- 26 "#773A01", -- 27 "#B7EA63", -- 28 "#764A4A", -- 29 "#7EB900", -- 30 } -- delete excess so a table shuffe wont use the colors added on the bottom if #ffaColors > #teamList-1 then for i = #teamList, #ffaColors do ffaColors[i] = nil end end -- Tailwind v4 color palette (Mostly brightness 200 to 800) -- Note: the official color palette used P3 colors (which are meant for HDR displays), this is the fallback RGB colors. local gradients = { blue = {{190, 219, 255}, {142, 197, 255}, {81, 162, 255}, {43, 127, 255}, {21, 93, 252}, {20, 71, 230}, {25, 60, 184}}, cyan = {{162, 244, 253}, {83, 234, 253}, {0, 211, 242}, {0, 184, 219}, {0, 146, 184}, {0, 117, 149}, {0, 95, 120}}, violet = {{221, 214, 255}, {196, 180, 255}, {166, 132, 255}, {142, 81, 255}, {127, 34, 254}, {112, 8, 231}, {93, 14, 192}, {77, 23, 154}}, fusia = {{246, 207, 255}, {244, 168, 255}, {237, 106, 255}, {225, 42, 251}, {200, 0, 222}, {168, 0, 183}, {138, 1, 148}}, pink = {{253, 165, 213}, {251, 100, 182}, {246, 51, 154}, {230, 0, 118}, {198, 0, 92}, {163, 0, 76}, {134, 16, 67}}, red = {{255, 201, 201}, {255, 162, 162}, {255, 100, 103}, {251, 44, 54}, {231, 0, 11}, {193, 0, 7}, {159, 7, 18}}, orange = {{255, 214, 167}, {255, 184, 106}, {255, 137, 4}, {255, 105, 0}, {245, 73, 0}, {202, 53, 0}}, yellow = {{255, 240, 133}, {255, 223, 32}, {253, 199, 0}, {240, 177, 0}, {208, 135, 0}, {166, 95, 0}, {137, 75, 0}}, green = {{185, 248, 207}, {123, 241, 168}, {5, 223, 114}, {0, 201, 80}, {0, 166, 62}, {0, 130, 54}, {1, 102, 48}}, lime = {{236, 252, 202}, {216, 249, 153}, {187, 244, 81}, {154, 230, 0}, {124, 207, 0}, {94, 165, 0}, {73, 125, 0}, {60, 99, 0}, {53, 83, 14}}, teal = {{150, 247, 228}, {70, 236, 213}, {0, 213, 190}, {0, 187, 167}, {0, 150, 137}, {0, 120, 111}, {0, 95, 90}}, sky = {{184, 230, 254}, {116, 212, 255}, {0, 188, 255}, {0, 166, 244}, {0, 132, 209}, {0, 105, 168}, {0, 89, 138}}, amber = {{254, 230, 133}, {255, 210, 48}, {255, 185, 0}, {254, 154, 0}, {225, 113, 0}, {187, 77, 0}, {151, 60, 0}}, -- very similar to orange, so only use it in the 4 teams case } local allGrandientNames = {"blue", "red", "green", "yellow", "fusia", "teal", "orange", "pink", "lime", "violet", "cyan", "sky"} local gradientGroupPerNumTeams = { -- One team { {"blue", "sky", "violet", "green", "lime"}, -- cold }, -- 2 teams { {"blue", "sky", "violet", "green", "lime"}, -- cold {"red", "pink", "fusia", "orange", "yellow"}, -- warm }, -- 3 teams { {"blue", "sky", "violet"}, -- blue {"red", "orange", "yellow"}, -- red {"green", "lime", "teal"}, -- green }, -- 4 teams { {"blue", "sky", "violet"}, -- blue {"red", "pink", "fusia"}, -- red pink {"green", "lime", "teal"}, -- green {"orange", "yellow"}, -- yellow orange }, -- 5 teams { {"blue", "sky"}, -- blue {"red", "pink"}, -- red pink {"green", "lime"}, -- green {"orange", "yellow"}, {"violet", "fusia"}, }, -- 6 teams { {"blue", "sky"}, -- blue {"red", "pink"}, -- red pink {"green", "lime"}, -- green {"orange", "yellow"}, {"violet", "fusia"}, {"teal", "cyan"}, }, } local survivalColors = { "#0B3EF3", -- 1 "#FF1005", -- 2 "#0CE908", -- 3 "#ffab8c", -- 4 "#09F5F5", -- 5 "#FCEEA4", -- 6 "#097E1C", -- 7 "#F190B3", -- 8 "#F80889", -- 9 "#3EFFA2", -- 10 "#911806", -- 11 "#7CA1FF", -- 12 "#3c7a74", -- 13 "#B04523", -- 14 "#B4FF39", -- 15 "#773A01", -- 16 "#D8EEFF", -- 17 "#689E3D", -- 18 "#0B849B", -- 19 "#FFD200", -- 20 "#971C48", -- 21 "#4A4376", -- 22 "#764A4A", -- 23 "#4F2684", -- 24 } local teamColors = { { -- One Team (not possible) { -- First Team "#004DFF", -- Armada Blue }, }, { -- Two Teams (40 colors) { -- First Team (Cool) "#0B3EF3", --1 "#0CE908", --2 "#00f5e5", --3 "#6941f2", --4 "#8fff94", --5 "#1b702f", --6 "#7cc2ff", --7 "#a294ff", --8 "#0B849B", --9 "#689E3D", --10 "#4F2684", --11 "#2C32AC", --12 "#6968A0", --13 "#D8EEFF", --14 "#3475FF", --15 "#7EB900", --16 "#4A4376", --17 "#B7EA63", --18 "#C4A9FF", --19 "#37713A", --20 }, { -- Second Team (Warm) "#FF1005", --1 "#FFD200", --2 "#FF6107", --3 "#F80889", --4 "#FCEEA4", --5 "#8a2828", --6 "#F190B3", --7 "#C88B2F", --8 "#B04523", --9 "#FFBB7C", --10 "#A35274", --11 "#773A01", --12 "#F5A200", --13 "#BBA28B", --14 "#971C48", --15 "#FF68EA", --16 "#DD783F", --17 "#FFAAF3", --18 "#764A4A", --19 "#9F0D05", --20 }, }, { -- Three Teams (24 colors) { -- First Team (Blue) "#004DFF", -- 1 "#09F5F5", -- 2 "#7CA1FF", -- 3 "#2C32AC", -- 4 "#D8EEFF", -- 5 "#0B849B", -- 6 "#3C7AFF", -- 7 "#5F6492", -- 8 }, { -- Second Team (Red) "#FF1005", -- 1 "#FF6107", -- 2 "#FFD200", -- 3 "#FF6058", -- 4 "#FFBB7C", -- 5 "#C88B2F", -- 6 "#F5A200", -- 7 "#9F0D05", -- 8 }, { -- Third Team (Green) "#0CE818", -- 1 "#B4FF39", -- 2 "#097E1C", -- 3 "#3EFFA2", -- 4 "#689E3D", -- 5 "#7EB900", -- 6 "#B7EA63", -- 7 "#37713A", -- 8 }, }, { -- Four Teams (24 colors) { -- First Team (Blue) "#004DFF", -- 1 "#7CA1FF", -- 2 "#D8EEFF", -- 3 "#09F5F5", -- 4 "#3475FF", -- 5 "#0B849B", -- 6 }, { -- Second Team (Red) "#FF1005", -- 1 "#FF6107", -- 2 "#FF6058", -- 3 "#B04523", -- 4 "#F80889", -- 5 "#971C48", -- 6 }, { -- Third Team (Green) "#0CE818", -- 1 "#B4FF39", -- 2 "#097E1C", -- 3 "#3EFFA2", -- 4 "#689E3D", -- 5 "#7EB900", -- 6 }, { -- Fourth Team (Yellow) "#FFD200", -- 1 "#F5A200", -- 2 "#FCEEA4", -- 3 "#FFBB7C", -- 4 "#BBA28B", -- 5 "#C88B2F", -- 6 }, }, { -- Five Teams (25 colors) { -- First Team (Blue) "#004DFF", -- 1 "#7CA1FF", -- 2 "#D8EEFF", -- 3 "#09F5F5", -- 4 "#3475FF", -- 5 }, { -- Second Team (Red) "#FF1005", -- 1 "#FF6107", -- 2 "#FF6058", -- 3 "#B04523", -- 4 "#9F0D05", -- 5 }, { -- Third Team (Green) "#0CE818", -- 1 "#B4FF39", -- 2 "#097E1C", -- 3 "#3EFFA2", -- 4 "#689E3D", -- 5 }, { -- Fourth Team (Yellow) "#FFD200", -- 1 "#F5A200", -- 2 "#FCEEA4", -- 3 "#FFBB7C", -- 4 "#C88B2F", -- 5 }, { -- Fifth Team (Fuchsia) "#F80889", -- 1 "#FF68EA", -- 2 "#FFAAF3", -- 3 "#AA0092", -- 4 "#701162", -- 5 }, }, { -- Six Teams (24 colors) { -- First Team (Blue) "#004DFF", -- 1 "#7CA1FF", -- 2 "#D8EEFF", -- 3 "#2C32AC", -- 4 }, { -- Second Team (Red) "#FF1005", -- 1 "#FF6058", -- 2 "#B04523", -- 3 "#9F0D05", -- 4 }, { -- Third Team (Green) "#0CE818", -- 1 "#B4FF39", -- 2 "#097E1C", -- 3 "#3EFFA2", -- 4 }, { -- Fourth Team (Yellow) "#FFD200", -- 1 "#F5A200", -- 2 "#FCEEA4", -- 3 "#9B6408", -- 4 }, { -- Fifth Team (Fuchsia) "#F80889", -- 1 "#FF68EA", -- 2 "#FFAAF3", -- 3 "#971C48", -- 4 }, { -- Sixth Team (Orange) "#FF6107", -- 1 "#FFBB7C", -- 2 "#DD783F", -- 3 "#773A01", -- 4 }, }, { -- Seven Teams (21 colors) { -- First Team (Blue) "#004DFF", -- 1 "#7CA1FF", -- 2 "#2C32AC", -- 3 }, { -- Second Team (Red) "#FF1005", -- 1 "#FF6058", -- 2 "#9F0D05", -- 3 }, { -- Third Team (Green) "#0CE818", -- 1 "#B4FF39", -- 2 "#097E1C", -- 3 }, { -- Fourth Team (Yellow) "#FFD200", -- 1 "#F5A200", -- 2 "#FCEEA4", -- 3 }, { -- Fifth Team (Fuchsia) "#F80889", -- 1 "#FF68EA", -- 2 "#FFAAF3", -- 3 }, { -- Sixth Team (Orange) "#FF6107", -- 1 "#FFBB7C", -- 2 "#DD783F", -- 3 }, { -- Seventh Team (Cyan) "#09F5F5", -- 1 "#0B849B", -- 2 "#D8EEFF", -- 3 }, }, { -- Eight Teams (24 colors) { -- First Team (Blue) "#004DFF", -- 1 "#7CA1FF", -- 2 "#2C32AC", -- 3 }, { -- Second Team (Red) "#FF1005", -- 1 "#FF6058", -- 2 "#9F0D05", -- 3 }, { -- Third Team (Green) "#0CE818", -- 1 "#B4FF39", -- 2 "#097E1C", -- 3 }, { -- Fourth Team (Yellow) "#FFD200", -- 1 "#F5A200", -- 2 "#FCEEA4", -- 3 }, { -- Fifth Team (Fuchsia) "#F80889", -- 1 "#FF68EA", -- 2 "#971C48", -- 3 }, { -- Sixth Team (Orange) "#FF6107", -- 1 "#FFBB7C", -- 2 "#DD783F", -- 3 }, { -- Seventh Team (Cyan) "#09F5F5", -- 1 "#0B849B", -- 2 "#D8EEFF", -- 3 }, { -- Eigth Team (Purple) "#872DFA", -- 1 "#6809A1", -- 2 "#C4A9FF", -- 3 }, }, } local r = math.random(1,100000) math.randomseed(1) -- make sure the next sequence of randoms can be reproduced local teamRandoms = {} for i = 1, #teamList do teamRandoms[teamList[i]] = { math.random(), math.random(), math.random() } end math.randomseed(r) local iconDevModeColors = { armblue = armBlueColor, corred = corRedColor, scavpurp = scavPurpColor, raptororange = raptorOrangeColor, gaiagray = gaiaGrayColor, leggren = legGreenColor, } local iconDevMode = Spring.GetModOptions().teamcolors_icon_dev_mode local iconDevModeColor = iconDevModeColors[iconDevMode] local function interpolateGradient(gradient, percentage) if percentage >= 1.0 then return gradient[#gradient] end local colorPercentage = percentage * (#gradient - 1) local colorA = gradient[math.floor(colorPercentage) + 1] local colorB = gradient[math.floor(colorPercentage) + 2] local lerpRatio = colorPercentage - math.floor(colorPercentage) return { math.clamp(math.floor((colorB[1] - colorA[1]) * lerpRatio + colorA[1]), 0, 255), math.clamp(math.floor((colorB[2] - colorA[2]) * lerpRatio + colorA[2]), 0, 255), math.clamp(math.floor((colorB[3] - colorA[3]) * lerpRatio + colorA[3]), 0, 255)} end local function shuffleTable(Table) local originalTable = {} table.append(originalTable, Table) local shuffledTable = {} if #originalTable > 0 then repeat local r = math.random(#originalTable) table.insert(shuffledTable, originalTable[r]) table.remove(originalTable, r) until #originalTable == 0 else shuffledTable = originalTable end return shuffledTable end local function shuffleAllColors() ffaColors = shuffleTable(ffaColors) survivalColors = shuffleTable(survivalColors) for i = 1, #teamColors do for j = 1, #teamColors[i] do teamColors[i][j] = shuffleTable(teamColors[i][j]) end end end local function hex2RGB(hex) hex = hex:gsub("#", "") return { tonumber("0x" .. hex:sub(1, 2)), tonumber("0x" .. hex:sub(3, 4)), tonumber("0x" .. hex:sub(5, 6)) } end -- we don't want to use FFA colors for TeamFFA, because we want each team to have its own color theme local useFFAColors = Spring.Utilities.Gametype.IsFFA() and not Spring.Utilities.Gametype.IsTeams() if not useFFAColors and not teamColors[allyTeamCount] and not isSurvival then -- Edge case for TeamFFA with more than supported number of teams useFFAColors = true end local teamColorsTable = {} local trueTeamColorsTable = {} -- run first as if we were specs so when we become specs, we can restore the true intended team colors local trueFfaColors = table.copy(ffaColors) -- run first as if we were specs so when we become specs, we can restore the true intended ffa colors local trueSurvivalColors = table.copy(survivalColors) -- run first as if we were specs so when we become specs, we can restore the true intended survival colors local function setupTeamColor(teamID, allyTeamID, isAI, localRun) if iconDevModeColor then teamColorsTable[teamID] = { r = hex2RGB(iconDevModeColor)[1], g = hex2RGB(iconDevModeColor)[2], b = hex2RGB(iconDevModeColor)[3], } -- Simple Team Colors elseif localRun and (Spring.GetConfigInt("SimpleTeamColors", 0) == 1 or (anonymousMode == "allred" and not mySpecState)) then Spring.Echo("Simple team color") local brightnessVariation = 0 local maxColorVariation = 0 if Spring.GetConfigInt("SimpleTeamColorsUseGradient", 0) == 1 then local totalEnemyDimmingCount = 0 for allyTeamID, count in pairs(dimmingCount) do if allyTeamID ~= myAllyTeamID then totalEnemyDimmingCount = totalEnemyDimmingCount + count end end brightnessVariation = (0.7 - ((1 / #Spring.GetTeamList(allyTeamID)) * dimmingCount[allyTeamID])) * 255 brightnessVariation = brightnessVariation * math.min((#Spring.GetTeamList(allyTeamID) * 0.8)-1, 1) -- dont change brightness too much in tiny teams maxColorVariation = 60 end local color = hex2RGB(ffaColors[allyTeamID+1] or '#333333') if teamID == gaiaTeamID then brightnessVariation = 0 maxColorVariation = 0 color = hex2RGB(gaiaGrayColor) elseif teamID == myTeamID then brightnessVariation = 0 maxColorVariation = 0 color = {Spring.GetConfigInt("SimpleTeamColorsPlayerR", 0), Spring.GetConfigInt("SimpleTeamColorsPlayerG", 77), Spring.GetConfigInt("SimpleTeamColorsPlayerB", 255)} elseif allyTeamID == myAllyTeamID then color = {Spring.GetConfigInt("SimpleTeamColorsAllyR", 0), Spring.GetConfigInt("SimpleTeamColorsAllyG", 255), Spring.GetConfigInt("SimpleTeamColorsAllyB", 0)} elseif allyTeamID ~= myAllyTeamID then color = {Spring.GetConfigInt("SimpleTeamColorsEnemyR", 255), Spring.GetConfigInt("SimpleTeamColorsEnemyG", 16), Spring.GetConfigInt("SimpleTeamColorsEnemyB", 5)} end color[1] = math.min(color[1] + brightnessVariation, 255) + ((teamRandoms[teamID][1] * (maxColorVariation * 2)) - maxColorVariation) color[2] = math.min(color[2] + brightnessVariation, 255) + ((teamRandoms[teamID][2] * (maxColorVariation * 2)) - maxColorVariation) color[3] = math.min(color[3] + brightnessVariation, 255) + ((teamRandoms[teamID][3] * (maxColorVariation * 2)) - maxColorVariation) teamColorsTable[teamID] = { r = color[1], g = color[2], b = color[3], } elseif isAI and string.find(isAI, "Scavenger") then print("Scavenger team color") teamColorsTable[teamID] = { r = hex2RGB(scavPurpColor)[1], g = hex2RGB(scavPurpColor)[2], b = hex2RGB(scavPurpColor)[3], } elseif isAI and string.find(isAI, "Raptor") then print("Raptor team color") teamColorsTable[teamID] = { r = hex2RGB(raptorOrangeColor)[1], g = hex2RGB(raptorOrangeColor)[2], b = hex2RGB(raptorOrangeColor)[3], } elseif teamID == gaiaTeamID then Spring.Echo("gaia team color") teamColorsTable[teamID] = { r = hex2RGB(gaiaGrayColor)[1], g = hex2RGB(gaiaGrayColor)[2], b = hex2RGB(gaiaGrayColor)[3], } elseif isSurvival and survivalColors[#Spring.GetTeamList()-2] then print("survivor team color") teamColorsTable[teamID] = { r = hex2RGB(survivalColors[survivalColorNum])[1] + math.random(-survivalColorVariation, survivalColorVariation), g = hex2RGB(survivalColors[survivalColorNum])[2] + math.random(-survivalColorVariation, survivalColorVariation), b = hex2RGB(survivalColors[survivalColorNum])[3] + math.random(-survivalColorVariation, survivalColorVariation), } survivalColorNum = survivalColorNum + 1 -- Will start from the next color next time -- Use procedural colors elseif -- Number of player in last non-gaia team is larger than one and there is not a color palette for it (#Spring.GetTeamList(allyTeamCount-1) > 1 and (not teamColors[allyTeamCount] or not teamColors[allyTeamCount][1][#Spring.GetTeamList(allyTeamCount-1)])) -- or There is more than 29 players (ignores gaia) or #Spring.GetTeamList() -1 > #ffaColors then local totalNumAllyTeams = allyTeamCount local overallNumPlayerPerTeam = #Spring.GetTeamList(allyTeamCount-1) local numPlayerInTeam = #Spring.GetTeamList(allyTeamID) local nthPlayerInTeam = dimmingCount[allyTeamID] - 1 local useGradientGroup = not (not gradientGroupPerNumTeams[totalNumAllyTeams]) local color = {125, 125, 125} -- If gradient groups are used, each ally team is assigned N gradients if useGradientGroup then local teamGradientGroup = gradientGroupPerNumTeams[totalNumAllyTeams][allyTeamID + 1] local colorWithinGradient = color -- (unlikely edge case) If there are more gradient than player, just take the middle color of the gradient if numPlayerInTeam <= #teamGradientGroup then local gradient = gradients[teamGradientGroup[nthPlayerInTeam + 1]] local percentageWithinGradient = 0.5 colorWithinGradient = interpolateGradient(gradient, percentageWithinGradient) else -- Regular case, where we sample thru the group of gradient local playerGradientPercentage = nthPlayerInTeam / numPlayerInTeam local playerGradientId = math.floor(playerGradientPercentage * #teamGradientGroup) local playerGradientName = teamGradientGroup[playerGradientId+1] local gradient = gradients[playerGradientName] local startGradientPercentage = playerGradientId / #teamGradientGroup local endGradientPercentage = (playerGradientId + 1) / #teamGradientGroup local percentageWithinGradient = (playerGradientPercentage - startGradientPercentage) / (endGradientPercentage - startGradientPercentage) colorWithinGradient = interpolateGradient(gradient, percentageWithinGradient) end -- local colorWithinGradient = gradient[math.floor(percentageWithinGradient * #gradient) + 1] color[1] = colorWithinGradient[1] color[2] = colorWithinGradient[2] color[3] = colorWithinGradient[3] else -- Each ally team use one gradient, this gradient might be share with other ally team(s) (e.g one team is bright green, another is dark green) local gradientId = math.floor(allyTeamID % #allGrandientNames) local gradName = allGrandientNames[gradientId + 1] local gradient = gradients[gradName] -- How many ally team share the same gradient? local numAllyTeamWithGradient = math.floor(totalNumAllyTeams / #allGrandientNames) -- Not all gradients will share the same number of ally team if gradientId < totalNumAllyTeams % #allGrandientNames then numAllyTeamWithGradient = numAllyTeamWithGradient + 1 end local nthAllyTeamWithGradient = math.floor(allyTeamID / #allGrandientNames) -- If there is only a single player in a team, take the color at the center of the gradient local playerGradientPercentage = 0.0 if numPlayerInTeam <= 1 then playerGradientPercentage = 0.5 -- Otherwise, we distribute the players amount the gradient else playerGradientPercentage = nthPlayerInTeam / (numPlayerInTeam-1) local percentagePerPlayer = 1.0 / (numPlayerInTeam-1) -- When there are fewer than 5 players in an ally team, the brighness change between player is large, so instead of sampling the entire gradient -- we only sample near the middle of the gradient. local maxPerPlayerPercentage = 0.25 if percentagePerPlayer > maxPerPlayerPercentage then -- Basically, space every player by 25% of the gradient, but the range is centered on the middle of the gradient playerGradientPercentage = maxPerPlayerPercentage * nthPlayerInTeam + (1.0 - maxPerPlayerPercentage * numPlayerInTeam) / 2.0 end end -- If multiple ally team share the same gradient, create a gap between the end of one team's and the start of the next one if numAllyTeamWithGradient > 1 and nthAllyTeamWithGradient + 1 ~= numAllyTeamWithGradient then playerGradientPercentage = 0.9 * playerGradientPercentage end local gradientPercentage = playerGradientPercentage / numAllyTeamWithGradient + nthAllyTeamWithGradient / numAllyTeamWithGradient local colorWithinGradient = interpolateGradient(gradient, gradientPercentage) color[1] = colorWithinGradient[1] color[2] = colorWithinGradient[2] color[3] = colorWithinGradient[3] end teamColorsTable[teamID] = { r = color[1], g = color[2], b = color[3], } -- auto ffa gradient colored for huge player games elseif useFFAColors or -- or Number of player in last non-gaia team is larger than one and there is not a color palette for it (#Spring.GetTeamList(allyTeamCount-1) > 1 and (not teamColors[allyTeamCount] or not teamColors[allyTeamCount][1][#Spring.GetTeamList(allyTeamCount-1)])) -- or There is more than 29 players (ignores gaia) or #Spring.GetTeamList() > 30 -- or the number of players in the last non-gaia team is one and there is more than team than ffaColor or (#Spring.GetTeamList(allyTeamCount-1) == 1 and not ffaColors[allyTeamCount]) then Spring.Echo("Random team color useFFAColors =", useFFAColors, " #Spring.GetTeamList() =", #Spring.GetTeamList(), " #Spring.GetTeamList(allyTeamCount-1)=", #Spring.GetTeamList(allyTeamCount-1)) local color = hex2RGB(ffaColors[allyTeamID+1] or '#333333') -- maxIteration = floor((num_ally_team-1) / 30) local maxIterations = math.floor((allyTeamID+1)/(#ffaColors)) -- local maxIterations = math.floor((allyTeamID+1) / #ffaColors) -- dimmingcount here is you're the Nth player of this team to process -- So if you're the Nth player of a team with M players: -- brightnessVariation = (0.6 - N / M) * 255 , so the range is in -0.4*255 to 0.6*255 or -102 to 153 local brightnessVariation = (0.6 - ((1 / #Spring.GetTeamList(allyTeamID)) * dimmingCount[allyTeamID])) * 255 -- brightnessVariation *= math.min(M*0.7 - 1, 1) -- For M == 1 -> -0.3 -- For M == 2 -> 0.4 -- For M >= 3 -> 1 brightnessVariation = brightnessVariation * math.min((#Spring.GetTeamList(allyTeamID) * 0.7)-1, 1) -- dont change brightness too much in tiny teams -- Basically maxColorVariation gets lower and lower as the number of team increase -- maxColorVariation = 120 / max(1, num_team) -- So for #P team 1 2 3 4 5 6 7 8 -- maxColorVariation = 120, 60, 40, 30, 24, 20, 17, 15... local maxColorVariation = (120 / math.max(1, allyTeamCount-1)) if #Spring.GetTeamList(allyTeamID) == 1 then brightnessVariation = 0 maxColorVariation = 0 end -- Basically if there are more player than the table of ffacolors -- This is make no sense, because this is trying to add a per team variation, instead of a per player variation if maxIterations > 1 then -- iteration = 1 + math.floor((allyTeamID+1) / 30 ) -- iteration = 1 + (allyTeamID+1) // 30 local iteration = 1 + math.floor((allyTeamID+1)/(#ffaColors)) -- ffacolor = (allyTeamID+1) - (#ffaColors*(iteration-1)) + 1 -- = (allyTeamID+1) - (30*((allyTeamID+1) // 30)) + 1 -- = (allyTeamID+1) % 30 local ffaColor = (allyTeamID+1) - (#ffaColors*(iteration-1)) + 1 if iteration ~= 1 then color = hex2RGB(ffaColors[ffaColor]) end if iteration == 1 then color[1] = color[1] + 40 color[2] = color[2] + 40 color[3] = color[3] + 40 elseif iteration == 2 then color[1] = color[1] - 70 color[2] = color[2] - 70 color[3] = color[3] - 70 elseif iteration == 3 then color[1] = color[1] + 130 color[2] = color[2] + 130 color[3] = color[3] + 130 end end if teamID == gaiaTeamID then brightnessVariation = 0 maxColorVariation = 0 color = hex2RGB(gaiaGrayColor) end -- clamp(floor( r + brightnessVariation + 2 * random * maxColorVariation - maxColorVariation) -- ), 0, 255) -- so: -- clamp(floor( r + brightnessVariation + (2 * random - 1.0) * maxColorVariation) -- ), 0, 255) -- So: -- clamp(floor( r + brightnessVariation + random(-1., 1.) * maxColorVariation) -- ), 0, 255) -- brightnessVariation is between -0.4*255 to 0.6*255 for large team color[1] = math.clamp(math.floor(color[1] + brightnessVariation + ((teamRandoms[teamID][1] * (maxColorVariation * 2)) - maxColorVariation)), 0, 255) color[2] = math.clamp(math.floor(color[2] + brightnessVariation + ((teamRandoms[teamID][2] * (maxColorVariation * 2)) - maxColorVariation)), 0, 255) color[3] = math.clamp(math.floor(color[3] + brightnessVariation + ((teamRandoms[teamID][3] * (maxColorVariation * 2)) - maxColorVariation)), 0, 255) teamColorsTable[teamID] = { r = color[1], g = color[2], b = color[3], } else Spring.Echo("Palette team color") if not teamSizes[allyTeamID] then allyTeamNum = allyTeamNum + 1 teamSizes[allyTeamID] = { allyTeamNum, 1, 0 } -- Team number, Starting color number, Color variation end if teamColors[allyTeamCount] -- If we have the color set for this number of teams and teamColors[allyTeamCount][teamSizes[allyTeamID][1]] then -- And this team number exists in the color set if not teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]] then -- If we have no color for this player anymore teamSizes[allyTeamID][2] = 1 -- Starting from the first color again.. end -- Assigning R,G,B values with specified color variations teamColorsTable[teamID] = { r = hex2RGB(teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]])[1] + math.random(-teamSizes[allyTeamID][3], teamSizes[allyTeamID][3]), g = hex2RGB(teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]])[2] + math.random(-teamSizes[allyTeamID][3], teamSizes[allyTeamID][3]), b = hex2RGB(teamColors[allyTeamCount][teamSizes[allyTeamID][1]][teamSizes[allyTeamID][2]])[3] + math.random(-teamSizes[allyTeamID][3], teamSizes[allyTeamID][3]), } teamSizes[allyTeamID][2] = teamSizes[allyTeamID][2] + 1 -- Will start from the next color next time else Spring.Echo("[AUTOCOLORS] Error: Team Colors Table is broken or missing for this allyteam set") teamColorsTable[teamID] = { r = 255, g = 255, b = 255, } end end end local function setupAllTeamColors(localRun) survivalColorNum = 1 -- Starting from color #1 survivalColorVariation = 0 -- Current color variation allyTeamNum = 0 teamSizes = {} dimmingCount = {} for _, allyTeamID in ipairs(Spring.GetAllyTeamList()) do dimmingCount[allyTeamID] = 0 end for i = 1, #teamList do local teamID = teamList[i] local allyTeamID = select(6, Spring.GetTeamInfo(teamID)) dimmingCount[allyTeamID] = dimmingCount[allyTeamID] + 1 local isAI = Spring.GetTeamLuaAI(teamID) setupTeamColor(teamID, allyTeamID, isAI, localRun) end end setupAllTeamColors(false) trueTeamColorsTable = table.copy(teamColorsTable) -- store the true team colors so we can restore them when we become a spec if gadgetHandler:IsSyncedCode() then --- NOTE: STUFF DONE IN SYNCED IS FOR REPLAY WEBSITE local AutoColors = {} for i = 1, #teamList do local teamID = teamList[i] AutoColors[i] = { teamID = teamID, r = trueTeamColorsTable[teamID].r, g = trueTeamColorsTable[teamID].g, b = trueTeamColorsTable[teamID].b, } end Spring.SendLuaRulesMsg("AutoColors" .. Json.encode(AutoColors)) else -- UNSYNCED local myPlayerID = Spring.GetLocalPlayerID() local mySpecState = Spring.GetSpectatingState() if anonymousMode == "local" then shuffleAllColors() end if anonymousMode == "local" or Spring.GetConfigInt("SimpleTeamColors", 0) == 1 then setupAllTeamColors(true) end local function isDiscoEnabled() return anonymousMode == "disco" and not mySpecState end -- shuffle colors for all teams except ourselves local function discoShuffle(myTeamID) -- store own color and do regular shuffle local myColor = teamColorsTable[myTeamID] shuffleAllColors() setupAllTeamColors(true) -- swap color with any team that might have been assigned own color local teamIDToSwapWith = nil for teamID, color in pairs(teamColorsTable) do if myColor.r == color.r and myColor.g == color.g and myColor.b == color.b then teamIDToSwapWith = teamID break end end if teamIDToSwapWith ~= nil then teamColorsTable[teamIDToSwapWith] = teamColorsTable[myTeamID] end -- restore own color teamColorsTable[myTeamID] = myColor end local function updateTeamColors() if isDiscoEnabled() then discoShuffle(Spring.GetMyTeamID()) end for teamID, color in pairs(teamColorsTable) do Spring.SetTeamColor(teamID, color.r / 255, color.g / 255, color.b / 255) end end updateTeamColors() local discoTimer = 0 local discoTimerThreshold = 2 * 60 -- shuffle every 2 minutes with disco mode enabled function gadget:Update() if isDiscoEnabled() then discoTimer = discoTimer + Spring.GetLastUpdateSeconds() if discoTimer > discoTimerThreshold then discoTimer = 0 updateTeamColors() end elseif Spring.GetConfigInt("UpdateTeamColors", 0) == 1 then setupAllTeamColors(true) updateTeamColors() Spring.SetConfigInt("UpdateTeamColors", 0) Spring.SetConfigInt("SimpleTeamColors_Reset", 0) end end function gadget:PlayerChanged(playerID) if playerID ~= myPlayerID then return end myAllyTeamID = Spring.GetMyAllyTeamID() local prevMyTeamID = myTeamID myTeamID = Spring.GetMyTeamID() if mySpecState and prevMyTeamID ~= myTeamID and Spring.GetConfigInt("SimpleTeamColors", 0) == 1 then Spring.SetConfigInt("UpdateTeamColors", 1) end if mySpecState ~= Spring.GetSpectatingState() then mySpecState = Spring.GetSpectatingState() teamColorsTable = table.copy(trueTeamColorsTable) ffaColors = table.copy(trueFfaColors) survivalColors = table.copy(trueSurvivalColors) Spring.SetConfigInt("UpdateTeamColors", 1) end end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,396 @@ # pip install matplotlib lupa colormath Pillow # Lua runtime from lupa.lua54 import LuaRuntime, LuaSyntaxError, LuaError # Figure visualization import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import numpy as np import pandas as pd from PIL import Image # Only used for computing the color divergence from colormath.color_objects import sRGBColor, LabColor from colormath.color_conversions import convert_color from colormath.color_diff import delta_e_cie2000 # Fix bug in asscalar of numpy def patch_asscalar(a): return a.item() setattr(np, "asscalar", patch_asscalar) def get_player_colors(number_teams, number_players): """Runs the game_autocolors.lua with the input number of team and number of players and returns the color of each player/team.""" lua = LuaRuntime(unpack_returned_tuples=True) player_ids = list(range(0, number_players)) team_id_to_players = {} number_player_per_team = number_players // number_teams current_team = [] teams = {} player_to_team_id = {} for player_id in player_ids: current_team.append(player_id) allies_team_id = len(teams) player_to_team_id[player_id] = allies_team_id if len(current_team) >= number_player_per_team: teams[allies_team_id] = current_team current_team = [] if len(current_team) > 0: teams[allies_team_id] = current_team # Gaia is always the last player and its part of its own team gaia_id = number_players teams[allies_team_id+1] = [gaia_id] player_to_team_id[gaia_id] = allies_team_id+1 player_ids.append(gaia_id) # print(teams) # print(list(teams.keys())) # Add input variables to the script allTeams = "{" + ','.join([str(id) for id in player_ids]) + "}" alliesTeamsToTeam = str(list(teams.values())).replace(':', '=').replace('[', '{').replace(']', '}') allAlliesTeamsId = str(list(teams.keys())).replace('[', '{').replace(']', '}') teamToAliesTeamId = str(list(player_to_team_id.values())).replace('[', '{').replace(']', '}') input_player_table = f""" gaia_id = {gaia_id} allTeams = {allTeams} alliesTeamsToTeam = {alliesTeamsToTeam} allAlliesTeamsId = {allAlliesTeamsId} teamToAliesTeamId = {teamToAliesTeamId} """ # print(input_player_table) # This is the minimum amount of mock to get game_autocolors.lua to run lua.require('math') preampule = ''' colorByTeamIdOut = {} local Gametype = {} function Gametype:new () return {} end function Gametype:IsPvE () return false end function Gametype:IsFFA () return false end function Gametype:IsTeams () return true end local Spring = {Utilities = {Gametype = Gametype}} function Spring:new () return {} end function Spring.Echo(...) print("spring>", ...) end function Spring:GetModOptions() return { teamcolors_icon_dev_mode = 'disabled', teamcolors_anonymous_mode = 'disabled', } end function Spring:GetGaiaTeamID() return gaia_id end function Spring:GetMyTeamID() return allTeams[1] end function Spring:GetLocalPlayerID() return allTeams[1] end function Spring:GetMyAllyTeamID() return teamToAliesTeamId[allTeams[1]] end -- List of players function Spring.GetTeamList(teamId) local teamList = {} local filterTeamId = -1 if teamId ~= nil then filterTeamId = teamId end for i, teamID in ipairs(allTeams) do if filterTeamId < 0 or teamToAliesTeamId[i] == filterTeamId then table.insert(teamList, teamID) end end return teamList end -- List of teams function Spring:GetAllyTeamList() return allAlliesTeamsId -- {0, 1} end function Spring.GetTeamLuaAI(teamID) return nil end function Spring:GetSpectatingState() return false, false, false end function IsPvE() return false end function Spring.GetTeamInfo(teamID, getTeamKeys) local numberleader = 0 local numberisDead = 0 local numberhasAI = 0 local stringside = "FooBar" local numberallyTeam = teamToAliesTeamId[teamID+1] local numberincomeMultiplier = 1.0 local customTeamKeys = {} return teamID, numberleader, numberisDead, numberhasAI, stringside, numberallyTeam, numberincomeMultiplier, customTeamKeys end function Spring:GetConfigInt(field, default) local options = { UpdateTeamColors = 0, SimpleTeamColors_Reset = 0, SimpleTeamColors = 0, } if options[field] ~= nil then return options[field] end return default end function Spring.SetTeamColor(teamID, r, g, b) -- todo -- print("f", teamID, r, g, b) color = { r, g, b } colorByTeamIdOut[teamID] = color end local gadgetHandler = {} function gadgetHandler:IsSyncedCode() return False end local gadget = {} function math.pow(a, b) return a ^ b end function table:copy(foo) return foo end function math.clamp(low, n, high) return math.min(math.max(n, low), high) end ''' # Execute game_autocolors.lua filepath = 'luarules/gadgets/game_autocolors.lua' # filepath = 'luarules/gadgets/game_autocolors_improved.lua' with open(filepath) as file: full_code = input_player_table + preampule + file.read() try: lua.execute(full_code) # When lua fails, print the line which cause the error except (LuaSyntaxError, LuaError) as err: err_str = str(err) if '[string "<python>"]:' in err_str: line_num = int(err_str.partition('[string "<python>"]:')[2].partition(':')[0]) lines = full_code.split('\n') for i in range(line_num-5, line_num+5): if i +1 == line_num: print(f"{i+1}>\t{lines[i]}") else: print(f"{i+1}:\t{lines[i]}") raise err # Extract output color of each player player_to_color = {} for team_id, rgb_table in dict(lua.globals().colorByTeamIdOut).items(): # print(f"team {team_id} : ", list(dict(rgb_table).values())) player_to_color[team_id] = (*list(dict(rgb_table).values()),) team_to_colors = {} for allies_team_id, players in teams.items(): colors = [player_to_color[id] for id in players] team_to_colors[allies_team_id] = colors return team_to_colors #num_players = 80 # num_player_per_team = 8 # num_team = 5 # Plot some num player / team + number of team permutation as their own figure for num_player_per_team, num_team in [(10, 3), (8, 8), (30, 2)]: num_players = num_player_per_team*num_team team_to_colors = get_player_colors(num_team, num_players) # Remove the last team, since this is Gaia team_to_colors = {k:v for k, v in team_to_colors.items() if k != len(team_to_colors)- 1} # print(team_to_colors) fig, axs = plt.subplots(nrows=len(team_to_colors)) fig.subplots_adjust(top=0.90, bottom=0.1, left=0.1, right=0.99, wspace=0.05) fig.suptitle(f'Autocolor for {num_player_per_team} players / team with {num_team} ally teams ({num_player_per_team * num_team} players in total)', fontsize=12) for ax, (team_id, player_colors) in zip(axs, team_to_colors.items()): # Convert the ordered list of colors to 1 x N x 3 array color_img = np.array([player_colors]) # Draw color map ax.imshow(color_img, aspect='auto', interpolation="none") # Add side text pos = list(ax.get_position().bounds) x_text = pos[0] - 0.01 y_text = pos[1] + pos[3]/2. fig.text(x_text, y_text, f"Team {team_id}", va='center', ha='right', fontfamily="Monospace", fontsize=10) # Turn off *all* ticks & spines, not just the ones with color maps. for ax in axs.flat: ax.set_axis_off() plt.savefig(f"autocolor_{num_player_per_team}pt_{num_team}t.png") # Compute all color for all team number and number of team permutation num_team_options = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 20, 24, 30, 50, 60, 120] num_player_per_team_options = [1, 2, 3, 4, 5, 8, 10, 12, 20, 30, 50, 60] # num_team_options = [7] # num_player_per_team_options = [8] big_color_table = [] for num_team in num_team_options: grid_row = [] for num_player_per_team in num_player_per_team_options: num_players = num_player_per_team * num_team if num_players > 255: grid_row.append([]) continue team_to_colors = get_player_colors(num_team, num_players) grid_row.append(team_to_colors) big_color_table.append(grid_row) # Compute LABCIE delta E color difference metric (very slow) enable_diff_metrics = False if enable_diff_metrics: color_diff_table = {"num_player_per_team": [], "num_team": [], "min_same_team_divergence": [], "min_enemy_team_divergence": []} for y, num_team in enumerate(num_team_options): for x, num_player_per_team in enumerate(num_player_per_team_options): num_players = num_player_per_team * num_team if num_players > 255: continue # Skip everything because this has N^4 complexity # if (not (num_player_per_team == 30 and num_team == 4)) and (not (num_player_per_team == 10 and num_team == 9)): # continue print(f"{num_player_per_team=}, {num_team=}") team_to_colors = big_color_table[y][x] color_diff_table["num_player_per_team"].append(num_player_per_team) color_diff_table["num_team"].append(num_team) # For each color in each team we compute the color distance to every other color in the same team min_divergence = None min_color = None for team_id, player_colors in team_to_colors.items(): for i, color in enumerate(player_colors): srgb_color = sRGBColor(*color) lab_color = convert_color(srgb_color, LabColor) for j in range(i+1, len(player_colors)): other_color = player_colors[j] srgb_other_color = sRGBColor(*other_color) lab_other_color = convert_color(srgb_other_color, LabColor) delta_e = delta_e_cie2000(lab_color, lab_other_color) if min_divergence is None or min_divergence > delta_e: min_divergence = delta_e min_color = (color, other_color) print("min_divergence same team", min_divergence, min_color) color_diff_table["min_same_team_divergence"].append(min_divergence) min_divergence = None for team_id, player_colors in team_to_colors.items(): for i, color in enumerate(player_colors): srgb_color = sRGBColor(*color) lab_color = convert_color(srgb_color, LabColor) for other_team_id, other_player_colors in team_to_colors.items(): if other_team_id == team_id: continue for i, other_color in enumerate(other_player_colors): srgb_other_color = sRGBColor(*other_color) lab_other_color = convert_color(srgb_other_color, LabColor) delta_e = delta_e_cie2000(lab_color, lab_other_color) if min_divergence is None or min_divergence > delta_e: min_divergence = delta_e min_color = (color, other_color) print("min_divergence different team", min_divergence, min_color) color_diff_table["min_enemy_team_divergence"].append(min_divergence) df = pd.DataFrame(color_diff_table) pd.set_option('display.max_rows', 500) pd.set_option('display.max_columns', 500) pd.set_option('display.width', 150) print("Same team divergence:") print(df.pivot_table(index="num_team", columns="num_player_per_team", values='min_same_team_divergence')) print("Enemy team divergence:") print(df.pivot_table(index="num_team", columns="num_player_per_team", values='min_enemy_team_divergence')) # Plot the huge figure of all possible team and number of player team variation plt.style.use('classic') fig = plt.figure(figsize=(4*len(num_player_per_team_options), 10*len(num_team_options))) outer = gridspec.GridSpec(nrows=len(num_team_options), ncols=len(num_player_per_team_options), figure=fig) # fig, axes = plt.subplots(nrows=len(num_team_options), ncols=len(num_player_per_team_options), figsize=(100, 100)) for y, num_team in enumerate(num_team_options): for x, num_player_per_team in enumerate(num_player_per_team_options): num_players = num_player_per_team * num_team if num_players > 255: continue raw_team_to_colors = big_color_table[y][x] # team_to_colors = get_player_colors(num_team, num_players) # Remove the last team, since this is Gaia team_to_colors = {k:v for k, v in raw_team_to_colors.items() if k != len(raw_team_to_colors)- 1} print(f"{num_team=} {num_player_per_team=}") inner = gridspec.GridSpecFromSubplotSpec(nrows=len(team_to_colors), ncols=1, subplot_spec=outer[y, x], wspace=0.1, hspace=0.1) for j, (team_id, player_colors) in enumerate(team_to_colors.items()): ax = fig.add_subplot(inner[j]) # ax = plt.Subplot(fig, inner[j]) # First team colorbar if j == 0: # First row of the figure if y == 0: ax.text(0.7*x, -0.55, f"{num_player_per_team}", fontsize=50, horizontalalignment='center', verticalalignment='bottom') ax.set_title(f"{num_player_per_team} players / team with {num_team} ally teams ({num_player_per_team * num_team} players in total)", fontsize=6, color="gray") # Middle colorbar of the first column of figures if x == 0 and j == len(team_to_colors) // 2: ax.text(-0.8, 0.0, f"{num_team}", fontsize=50, horizontalalignment='right', verticalalignment='center') # Convert the ordered list of player colors to 1 x N x 3 array color_img = np.array([player_colors]) # Draw color map ax.imshow(color_img, aspect='auto', interpolation="none") # ax.set_axis_off() ax.spines[['left', 'right','bottom', 'top']].set_visible(False) ax.get_xaxis().set_ticks([]) ax.get_yaxis().set_ticks([]) if len(team_to_colors) >= 20: if len(team_to_colors) < 100 or team_id % 4 == 0: ax.set_ylabel(f"{team_id}", fontfamily="Monospace", fontsize=5 if len(team_to_colors) > 24 else 8) else: fontsize = 10 if num_team >= 10: fontsize = 8 ax.set_ylabel(f"Team {team_id}", fontfamily="Monospace", fontsize=fontsize) fig.supxlabel("Number of Player in each team", y=0.91, fontsize=100, va="bottom") fig.supylabel("Number of Team", fontsize=100) img_path = "huge_team_img.png" plt.savefig(img_path) # Create thumbnail with Image.open(img_path) as im: im.thumbnail((512,-1), Image.Resampling.LANCZOS) # Save the new thumbnail image im.save(img_path.replace(".png", "_thumb.png"), "PNG")