@@ -245,33 +245,97 @@ function MP.UTILS.version_mismatches()
245245 return results
246246end
247247
248+ -- Mod-policy keys are matched case- and punctuation-insensitively, so staff can
249+ -- key the banned/approved lists by display name (e.g. "Joker Display") and still
250+ -- match the wire-reported SMODS id (e.g. "JokerDisplay" or "jokerdisplay").
251+ function MP .UTILS .normalize_mod_name (name )
252+ if type (name ) ~= " string" then return " " end
253+ return (name :lower ():gsub (" [^%w]" , " " ))
254+ end
255+
256+ -- Cache the normalized index per source table; keyed weakly by table identity so
257+ -- it auto-refreshes when the server replaces MP.BANNED_MODS / MP.APPROVED_MODS.
258+ local _norm_index_cache = setmetatable ({}, { __mode = " k" })
259+ local function normalized_index (map )
260+ if type (map ) ~= " table" then return {} end
261+ local cached = _norm_index_cache [map ]
262+ if cached then return cached end
263+ local idx = {}
264+ for k , v in pairs (map ) do
265+ idx [MP .UTILS .normalize_mod_name (k )] = v
266+ end
267+ _norm_index_cache [map ] = idx
268+ return idx
269+ end
270+
271+ -- A rule version matches the wire version on exact string OR on the X.Y.Z
272+ -- (clean semver) prefix, so a rule like "1.0.0" still matches "1.0.0~BETA-1620a"
273+ -- or "1.0.0-DEV". version_prefix is defined earlier in this file.
274+ local function version_matches (rule_version , version )
275+ if version == rule_version then return true end
276+ local rp = MP .UTILS .version_prefix (rule_version )
277+ local vp = MP .UTILS .version_prefix (version )
278+ return rp ~= nil and rp == vp
279+ end
280+
281+ -- A rule is true (any version), an exact version string, or a list of versions.
282+ local function rule_matches (rule , version )
283+ if rule == nil then return false end
284+ if type (rule ) == " boolean" then return rule end
285+ if type (rule ) == " string" then return version_matches (rule , version ) end
286+ if type (rule ) == " table" then
287+ for _ , v in ipairs (rule ) do
288+ if version_matches (v , version ) then return true end
289+ end
290+ end
291+ return false
292+ end
293+
294+ -- A version-like segment starts with a digit, 'v', or '~' (e.g. "0.2.2", "v1",
295+ -- "~BETA"). Used to guess where a mod id ends and its version begins.
296+ local function looks_like_version (seg )
297+ return seg ~= nil and seg :match (" ^[%dv~]" ) ~= nil
298+ end
299+
300+ -- Matches a parsed mod entry against a normalized index. A mod's version can
301+ -- contain dashes (e.g. Saturn "0.2.2-E-ALPHA"), and parse_modlist splits on the
302+ -- last dash, so mod_name can arrive with version fragments glued on
303+ -- ("Saturn-0.2.2-E"). Walk the dash-delimited prefixes and treat a prefix as the
304+ -- id only when the next segment looks like a version — so "Saturn|0.2.2" splits
305+ -- but a genuinely dashed id like "Saturn-Extras|1.0" stays intact. First (shortest)
306+ -- valid match wins. Heuristic, not airtight; special-case real collisions if any
307+ -- ever show up.
308+ local function index_match (idx , mod_name , mod_version )
309+ local segs = {}
310+ for seg in tostring (mod_name ):gmatch (" [^%-]+" ) do
311+ segs [# segs + 1 ] = seg
312+ end
313+ local candidate
314+ for i = 1 , # segs do
315+ candidate = candidate and (candidate .. " -" .. segs [i ]) or segs [i ]
316+ if segs [i + 1 ] == nil or looks_like_version (segs [i + 1 ]) then
317+ if rule_matches (idx [MP .UTILS .normalize_mod_name (candidate )], mod_version ) then return true end
318+ end
319+ end
320+ return false
321+ end
322+
323+ -- Returns "banned" | "approved" | "unknown". Banned takes precedence.
324+ function MP .UTILS .classify_mod (mod_name , mod_version )
325+ if index_match (normalized_index (MP .BANNED_MODS ), mod_name , mod_version ) then return " banned" end
326+ if index_match (normalized_index (MP .APPROVED_MODS ), mod_name , mod_version ) then return " approved" end
327+ return " unknown"
328+ end
329+
248330function MP .UTILS .get_banned_mods (mods )
249331 local banned_mods = {}
250332 if not mods then return banned_mods end
251333
334+ local idx = normalized_index (MP .BANNED_MODS )
252335 for mod_name , mod_version in pairs (mods ) do
253- local ban_info = MP .BANNED_MODS [mod_name ]
254- local is_banned = false
255-
256- if ban_info then
257- if type (ban_info ) == " boolean" then
258- -- Old format: ban all versions
259- is_banned = ban_info
260- elseif type (ban_info ) == " string" then
261- -- New format: ban specific version
262- is_banned = (mod_version == ban_info )
263- elseif type (ban_info ) == " table" then
264- -- Table format: ban multiple specific versions
265- for _ , banned_version in ipairs (ban_info ) do
266- if mod_version == banned_version then
267- is_banned = true
268- break
269- end
270- end
271- end
336+ if index_match (idx , mod_name , mod_version ) then
337+ table.insert (banned_mods , mod_name )
272338 end
273-
274- if is_banned then table.insert (banned_mods , mod_name ) end
275339 end
276340
277341 return banned_mods
0 commit comments