fix(learner): correct spawn iteration, gate AscensionDB overrides, clean fallback pollution
Real bug fixes (no behavior additions that risk regressions): - LearnNPC: require both spawnX/spawnY AND x/y before accepting a spawn entry. Previously quest-giver/finisher dialog polluted spawn lists with the player's current position. - _MergeSpawnEvidence: respect IsAscensionProtected ownership gate unconditionally. The 'test mode' comment-out is removed; AscensionDB curated spawn data is now protected from learner overwrite (fixes the Sunstrider Mana Wyrm pin regression). - _PruneSpawnOutliers and PruneLearnedSpawnOutliers: fix broken next() iteration. The old code used 'coord = next(spawnTable); while coord do ... coord = next(spawnTable, coord)' which treated the KEY as the value. For numeric spawn arrays this returned indices (1, 2, ...) but the code did coord[1] and coord[2] on those indices, which are nil. The outlier pruning was effectively dead code. Now iterates with 'coordKey, coord = next(spawnTable)' correctly, sorts toRemove keys descending for safe table.remove, and compacts numeric arrays. - InjectLearnedData: early-exit when learner disabled, strip [7] on initial npcDataOverrides creation (only _MergeSpawnEvidence should populate spawns), versioned cleanup that retroactively clears singleton-position spawn entries (≤1 unique position) from pollution caused by the prior LearnNPC bug. Supporting utilities added: CopyWithoutField, CountUniqueSpawnPositions, NormalizeCoordPair reuse (replaces inline floor(x*100+0.5)/100).
This commit is contained in:
+134
-62
@@ -200,6 +200,16 @@ local function NormalizeCoordPair(x, y)
|
|||||||
return nx, ny
|
return nx, ny
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function CopyWithoutField(data, skippedKey)
|
||||||
|
local copy = {}
|
||||||
|
for key, value in pairs(data) do
|
||||||
|
if key ~= skippedKey then
|
||||||
|
copy[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return copy
|
||||||
|
end
|
||||||
|
|
||||||
-- Returns the grid-bucket key for a coordinate so nearby points share the same slot
|
-- Returns the grid-bucket key for a coordinate so nearby points share the same slot
|
||||||
local function CoordBucket(x, y)
|
local function CoordBucket(x, y)
|
||||||
return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID
|
return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID
|
||||||
@@ -224,6 +234,29 @@ local function InsertIfNewBucket(coordList, x, y, customGrid)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function CountUniqueSpawnPositions(spawns)
|
||||||
|
if type(spawns) ~= "table" then return 0 end
|
||||||
|
|
||||||
|
local seen = {}
|
||||||
|
local count = 0
|
||||||
|
for _, coords in pairs(spawns) do
|
||||||
|
if type(coords) == "table" then
|
||||||
|
for _, coord in ipairs(coords) do
|
||||||
|
local x, y = NormalizeCoordPair(coord[1], coord[2])
|
||||||
|
if x and y then
|
||||||
|
local key = tostring(x) .. "," .. tostring(y)
|
||||||
|
if not seen[key] then
|
||||||
|
seen[key] = true
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
-- Detects if the current map is a "Micro-Dungeon" (small interior map)
|
-- Detects if the current map is a "Micro-Dungeon" (small interior map)
|
||||||
-- This is a heuristic: if we lack map data, we default to standard grid.
|
-- This is a heuristic: if we lack map data, we default to standard grid.
|
||||||
local function GetCustomGridPrecision()
|
local function GetCustomGridPrecision()
|
||||||
@@ -845,7 +878,7 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
|||||||
if zoneId and zoneId > 0 and not existing[9] then existing[9] = zoneId end
|
if zoneId and zoneId > 0 and not existing[9] then existing[9] = zoneId end
|
||||||
if factionString and not existing[13] then existing[13] = factionString end
|
if factionString and not existing[13] then existing[13] = factionString end
|
||||||
if subName and not existing[14] then existing[14] = subName end
|
if subName and not existing[14] then existing[14] = subName end
|
||||||
if x and y and zoneId and zoneId > 0 then
|
if spawnX and spawnY and x and y and zoneId and zoneId > 0 then
|
||||||
existing[7] = existing[7] or {}
|
existing[7] = existing[7] or {}
|
||||||
existing[7][zoneId] = existing[7][zoneId] or {}
|
existing[7][zoneId] = existing[7][zoneId] or {}
|
||||||
InsertIfNewBucket(existing[7][zoneId], x, y, GetCoordGridForZone(zoneId))
|
InsertIfNewBucket(existing[7][zoneId], x, y, GetCoordGridForZone(zoneId))
|
||||||
@@ -857,10 +890,15 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
|||||||
local threshold = (Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.minConfidencePins) or MIN_CONFIDENCE_PINS
|
local threshold = (Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.minConfidencePins) or MIN_CONFIDENCE_PINS
|
||||||
|
|
||||||
-- Live injection: update npcDataOverrides only if confidence threshold is met
|
-- Live injection: update npcDataOverrides only if confidence threshold is met
|
||||||
if existing.mc >= threshold and QuestieDB and QuestieDB.npcDataOverrides then
|
-- and only if the NPC has actual spawn data from kills (not just player-position fallback)
|
||||||
|
if existing.mc >= threshold and QuestieDB and QuestieDB.npcDataOverrides and existing[7] and next(existing[7]) then
|
||||||
local ovr = QuestieDB.npcDataOverrides[npcId]
|
local ovr = QuestieDB.npcDataOverrides[npcId]
|
||||||
if not ovr then
|
if not ovr then
|
||||||
|
if IsAscensionProtected("NPC", npcId, 7) then
|
||||||
|
QuestieDB.npcDataOverrides[npcId] = CopyWithoutField(existing, 7)
|
||||||
|
else
|
||||||
QuestieDB.npcDataOverrides[npcId] = existing
|
QuestieDB.npcDataOverrides[npcId] = existing
|
||||||
|
end
|
||||||
else
|
else
|
||||||
-- Merge: fill missing fields; also overwrite empty-string names
|
-- Merge: fill missing fields; also overwrite empty-string names
|
||||||
for k, v in pairs(existing) do
|
for k, v in pairs(existing) do
|
||||||
@@ -1053,10 +1091,8 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
entry.x = evidenceX
|
entry.x = evidenceX
|
||||||
entry.y = evidenceY
|
entry.y = evidenceY
|
||||||
|
|
||||||
-- Round to 2 decimal places for grouping
|
local rx, ry = NormalizeCoordPair(evidenceX, evidenceY)
|
||||||
local rx = floor(evidenceX * 100 + 0.5) / 100
|
local key = entry.zoneId .. "|" .. tostring(rx) .. "|" .. tostring(ry)
|
||||||
local ry = floor(evidenceY * 100 + 0.5) / 100
|
|
||||||
local key = entry.zoneId .. "|" .. rx .. "|" .. ry
|
|
||||||
-- DEBUG: log each entry being grouped
|
-- DEBUG: log each entry being grouped
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
"[QuestieLearner] _MergeSpawnEvidence GROUPING: spawnUID=", spawnUID,
|
"[QuestieLearner] _MergeSpawnEvidence GROUPING: spawnUID=", spawnUID,
|
||||||
@@ -1138,13 +1174,20 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
local promoted = 0
|
local promoted = 0
|
||||||
local duplicates = 0
|
local duplicates = 0
|
||||||
local zoneSpawns = QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId]
|
local zoneSpawns = QuestieDB.npcDataOverrides[npcId][7][topEvidence.zoneId]
|
||||||
local grid = GetCoordGridForZone(topEvidence.zoneId)
|
local seen = {}
|
||||||
for _, spawnEvidence in pairs(evidence) do
|
for _, spawnEvidence in pairs(evidence) do
|
||||||
if InsertIfNewBucket(zoneSpawns, spawnEvidence.x, spawnEvidence.y, grid) then
|
local sx, sy = NormalizeCoordPair(spawnEvidence.x, spawnEvidence.y)
|
||||||
|
if sx and sy and not seen[sx..","..sy] then
|
||||||
|
seen[sx..","..sy] = true
|
||||||
|
local grid = GetCoordGridForZone(topEvidence.zoneId)
|
||||||
|
if InsertIfNewBucket(zoneSpawns, sx, sy, grid) then
|
||||||
promoted = promoted + 1
|
promoted = promoted + 1
|
||||||
else
|
else
|
||||||
duplicates = duplicates + 1
|
duplicates = duplicates + 1
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
duplicates = duplicates + 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
@@ -1212,16 +1255,6 @@ local function _MergeSpawnEvidence(npcId)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test mode: allow learned Sunstrider spawn evidence to overwrite live
|
|
||||||
-- override data even if AscensionDB owns the field. If this restores the
|
|
||||||
-- Mana Wyrm pins, the ownership gate is the thing blocking live updates.
|
|
||||||
--[[ if IsAscensionProtected("NPC", npcId, 7) then
|
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
|
||||||
"[QuestieLearner] _MergeSpawnEvidence: npcId", npcId,
|
|
||||||
"spawn override skipped because AscensionDB owns this NPC spawn field")
|
|
||||||
return false
|
|
||||||
end --]]
|
|
||||||
|
|
||||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||||
"[QuestieLearner] _MergeSpawnEvidence promoting npcId", npcId,
|
"[QuestieLearner] _MergeSpawnEvidence promoting npcId", npcId,
|
||||||
"zone", tostring(topEvidence.zoneId),
|
"zone", tostring(topEvidence.zoneId),
|
||||||
@@ -1650,6 +1683,11 @@ end
|
|||||||
|
|
||||||
function QuestieLearner:InjectLearnedData()
|
function QuestieLearner:InjectLearnedData()
|
||||||
if not EnsureLearnedData() then return end
|
if not EnsureLearnedData() then return end
|
||||||
|
if not self:IsEnabled() then
|
||||||
|
QuestieLearner.data = Questie.dbLearner.global
|
||||||
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] InjectLearnedData skipped because learner is disabled")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local learned = Questie.dbLearner.global
|
local learned = Questie.dbLearner.global
|
||||||
-- Migrate old-format NPC data ([4]=spawns, [5]=zoneId) to new format ([7]=spawns, [9]=zoneId)
|
-- Migrate old-format NPC data ([4]=spawns, [5]=zoneId) to new format ([7]=spawns, [9]=zoneId)
|
||||||
@@ -1848,6 +1886,26 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged", purgedNpcs, "invalid NPCs from learned data")
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged", purgedNpcs, "invalid NPCs from learned data")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Versioned cleanup: strip spawns from NPCs learned via quest giver/finisher
|
||||||
|
-- fallback (player position stored as spawn by LearnNPC). These have only
|
||||||
|
-- a single spawn position regardless of confidence — real kill NPCs
|
||||||
|
-- accumulate spawns at multiple distinct locations.
|
||||||
|
local fallbackSpawnCleanupVersion = 2
|
||||||
|
if (learned._cleanedFallbackSpawnsVersion or 0) < fallbackSpawnCleanupVersion then
|
||||||
|
learned._cleanedFallbackSpawns = true
|
||||||
|
learned._cleanedFallbackSpawnsVersion = fallbackSpawnCleanupVersion
|
||||||
|
local stripped = 0
|
||||||
|
for npcId, data in pairs(learned.npcs) do
|
||||||
|
if data[7] and CountUniqueSpawnPositions(data[7]) <= 1 then
|
||||||
|
data[7] = nil
|
||||||
|
stripped = stripped + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if stripped > 0 then
|
||||||
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Stripped fallback spawns from", stripped, "NPCs")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Purge Object entries that duplicate NPC entries (mobs learned as both NPC and Object).
|
-- Purge Object entries that duplicate NPC entries (mobs learned as both NPC and Object).
|
||||||
-- NPC data is richer (has names, quest IDs), so keep the NPC version and remove the Object.
|
-- NPC data is richer (has names, quest IDs), so keep the NPC version and remove the Object.
|
||||||
local dupObjectsRemoved = 0
|
local dupObjectsRemoved = 0
|
||||||
@@ -1871,19 +1929,13 @@ function QuestieLearner:InjectLearnedData()
|
|||||||
end
|
end
|
||||||
self:Sanitize(data)
|
self:Sanitize(data)
|
||||||
if not QuestieDB.npcDataOverrides[nid or npcId] then
|
if not QuestieDB.npcDataOverrides[nid or npcId] then
|
||||||
QuestieDB.npcDataOverrides[nid or npcId] = data
|
-- Spawn evidence is promoted through _MergeSpawnEvidence, where
|
||||||
|
-- AscensionDB ownership is known. Injecting [7] here runs too early
|
||||||
|
-- and can pollute curated plugin spawn tables.
|
||||||
|
QuestieDB.npcDataOverrides[nid or npcId] = CopyWithoutField(data, 7)
|
||||||
npcCount = npcCount + 1
|
npcCount = npcCount + 1
|
||||||
else
|
else
|
||||||
local existing = QuestieDB.npcDataOverrides[nid or npcId]
|
local existing = QuestieDB.npcDataOverrides[nid or npcId]
|
||||||
if data[7] and not IsAscensionProtected("NPC", nid or npcId, 7) then
|
|
||||||
existing[7] = existing[7] or {}
|
|
||||||
for zoneId, coords in pairs(data[7]) do
|
|
||||||
existing[7][zoneId] = existing[7][zoneId] or {}
|
|
||||||
for _, coord in ipairs(coords) do
|
|
||||||
InsertIfNewBucket(existing[7][zoneId], coord[1], coord[2])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- Adopt other fields if missing
|
-- Adopt other fields if missing
|
||||||
for k, v in pairs(data) do
|
for k, v in pairs(data) do
|
||||||
if k ~= "mc" and k ~= 7 and existing[k] == nil and not IsAscensionProtected("NPC", nid or npcId, k) then
|
if k ~= "mc" and k ~= 7 and existing[k] == nil and not IsAscensionProtected("NPC", nid or npcId, k) then
|
||||||
@@ -2921,18 +2973,6 @@ end
|
|||||||
--- Uses a two-pass pattern so deletion always happens after traversal.
|
--- Uses a two-pass pattern so deletion always happens after traversal.
|
||||||
---@param spawnTable table The [zoneId] sub-table containing learned coords
|
---@param spawnTable table The [zoneId] sub-table containing learned coords
|
||||||
---@return table, number Array of keys to remove, count of keys
|
---@return table, number Array of keys to remove, count of keys
|
||||||
local function _CollectOutlierKeys(spawnTable)
|
|
||||||
local keys = {}
|
|
||||||
local n = 0
|
|
||||||
local coord = next(spawnTable)
|
|
||||||
while coord do
|
|
||||||
n = n + 1
|
|
||||||
keys[n] = coord
|
|
||||||
coord = next(spawnTable, coord)
|
|
||||||
end
|
|
||||||
return keys, n
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Prune wildly outlying learned spawn coords for a single NPC+zone entry.
|
--- Prune wildly outlying learned spawn coords for a single NPC+zone entry.
|
||||||
--- Only operates on QuestieLearner learned data (dbLearner.global.npcs / .objects).
|
--- Only operates on QuestieLearner learned data (dbLearner.global.npcs / .objects).
|
||||||
--- Never deletes static DB or AscensionDB spawns.
|
--- Never deletes static DB or AscensionDB spawns.
|
||||||
@@ -2949,11 +2989,13 @@ local function _PruneSpawnOutliers(spawnTable, zoneId, threshold)
|
|||||||
-- Pass 1: collect all points into a flat array
|
-- Pass 1: collect all points into a flat array
|
||||||
local learnedPoints = {}
|
local learnedPoints = {}
|
||||||
local n = 0
|
local n = 0
|
||||||
local coord = next(spawnTable)
|
local coordKey, coord = next(spawnTable)
|
||||||
while coord do
|
while coordKey do
|
||||||
|
if type(coord) == "table" and coord[1] and coord[2] then
|
||||||
n = n + 1
|
n = n + 1
|
||||||
learnedPoints[n] = { coord[1], coord[2] }
|
learnedPoints[n] = { coord[1], coord[2] }
|
||||||
coord = next(spawnTable, coord)
|
end
|
||||||
|
coordKey, coord = next(spawnTable, coordKey)
|
||||||
end
|
end
|
||||||
|
|
||||||
if n < 4 then return false end
|
if n < 4 then return false end
|
||||||
@@ -3004,21 +3046,36 @@ local function _PruneSpawnOutliers(spawnTable, zoneId, threshold)
|
|||||||
-- Pass 2: identify outlier keys (collect before deleting)
|
-- Pass 2: identify outlier keys (collect before deleting)
|
||||||
local toRemove = {}
|
local toRemove = {}
|
||||||
local rmCount = 0
|
local rmCount = 0
|
||||||
coord = next(spawnTable)
|
coordKey, coord = next(spawnTable)
|
||||||
while coord do
|
while coordKey do
|
||||||
|
if type(coord) == "table" and coord[1] and coord[2] then
|
||||||
local dx = abs(coord[1] - medianX)
|
local dx = abs(coord[1] - medianX)
|
||||||
local dy = abs(coord[2] - medianY)
|
local dy = abs(coord[2] - medianY)
|
||||||
if dx > pruneX or dy > pruneY then
|
if dx > pruneX or dy > pruneY then
|
||||||
rmCount = rmCount + 1
|
rmCount = rmCount + 1
|
||||||
toRemove[rmCount] = coord
|
toRemove[rmCount] = coordKey
|
||||||
end
|
end
|
||||||
coord = next(spawnTable, coord)
|
end
|
||||||
|
coordKey, coord = next(spawnTable, coordKey)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pass 3: delete in second pass (no mid-iteration table mutation)
|
table.sort(toRemove, function(a, b)
|
||||||
|
if type(a) == "number" and type(b) == "number" then
|
||||||
|
return a > b
|
||||||
|
end
|
||||||
|
return tostring(a) > tostring(b)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Pass 3: delete in second pass (no mid-iteration table mutation).
|
||||||
|
-- Numeric spawn arrays must be compacted so ipairs/# keep seeing later rows.
|
||||||
local removed = 0
|
local removed = 0
|
||||||
for i = 1, rmCount do
|
for i = 1, rmCount do
|
||||||
spawnTable[toRemove[i]] = nil
|
local key = toRemove[i]
|
||||||
|
if type(key) == "number" then
|
||||||
|
table.remove(spawnTable, key)
|
||||||
|
else
|
||||||
|
spawnTable[key] = nil
|
||||||
|
end
|
||||||
removed = removed + 1
|
removed = removed + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3061,22 +3118,24 @@ function QuestieLearner:PruneLearnedSpawnOutliers(threshold)
|
|||||||
local changed = false
|
local changed = false
|
||||||
|
|
||||||
-- Check if static DB has anchors for this NPC+zone
|
-- Check if static DB has anchors for this NPC+zone
|
||||||
local staticNPC = nil
|
local staticSpawns = nil
|
||||||
if QuestieDB and QuestieDB.QueryNPC then
|
if QuestieDB and QuestieDB.QueryNPC then
|
||||||
staticNPC = QuestieDB.QueryNPCSingle and QuestieDB.QueryNPCSingle(npcId, "spawns") or nil
|
staticSpawns = QuestieDB.QueryNPCSingle and QuestieDB.QueryNPCSingle(npcId, "spawns") or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
if staticNPC and staticNPC[7] and staticNPC[7][zoneId] then
|
if staticSpawns and staticSpawns[zoneId] then
|
||||||
-- Static anchor path: build centroid from static spawns
|
-- Static anchor path: build centroid from static spawns
|
||||||
local sc = staticNPC[7][zoneId]
|
local sc = staticSpawns[zoneId]
|
||||||
local si = next(sc)
|
local si, staticCoord = next(sc)
|
||||||
local sn = 0
|
local sn = 0
|
||||||
local sumX, sumY = 0, 0
|
local sumX, sumY = 0, 0
|
||||||
while si do
|
while si do
|
||||||
|
if type(staticCoord) == "table" and staticCoord[1] and staticCoord[2] then
|
||||||
sn = sn + 1
|
sn = sn + 1
|
||||||
sumX = sumX + sc[si][1]
|
sumX = sumX + staticCoord[1]
|
||||||
sumY = sumY + sc[si][2]
|
sumY = sumY + staticCoord[2]
|
||||||
si = next(sc, si)
|
end
|
||||||
|
si, staticCoord = next(sc, si)
|
||||||
end
|
end
|
||||||
|
|
||||||
if sn > 0 then
|
if sn > 0 then
|
||||||
@@ -3086,18 +3145,31 @@ function QuestieLearner:PruneLearnedSpawnOutliers(threshold)
|
|||||||
-- Collect outlier keys first, delete second
|
-- Collect outlier keys first, delete second
|
||||||
local toRemove = {}
|
local toRemove = {}
|
||||||
local rmCount = 0
|
local rmCount = 0
|
||||||
local coord = next(zoneSpawns)
|
local coordKey, coord = next(zoneSpawns)
|
||||||
while coord do
|
while coordKey do
|
||||||
|
if type(coord) == "table" and coord[1] and coord[2] then
|
||||||
local dx = abs(coord[1] - anchorX)
|
local dx = abs(coord[1] - anchorX)
|
||||||
local dy = abs(coord[2] - anchorY)
|
local dy = abs(coord[2] - anchorY)
|
||||||
if dx > threshold or dy > threshold then
|
if dx > threshold or dy > threshold then
|
||||||
rmCount = rmCount + 1
|
rmCount = rmCount + 1
|
||||||
toRemove[rmCount] = coord
|
toRemove[rmCount] = coordKey
|
||||||
end
|
end
|
||||||
coord = next(zoneSpawns, coord)
|
|
||||||
end
|
end
|
||||||
|
coordKey, coord = next(zoneSpawns, coordKey)
|
||||||
|
end
|
||||||
|
table.sort(toRemove, function(a, b)
|
||||||
|
if type(a) == "number" and type(b) == "number" then
|
||||||
|
return a > b
|
||||||
|
end
|
||||||
|
return tostring(a) > tostring(b)
|
||||||
|
end)
|
||||||
for i = 1, rmCount do
|
for i = 1, rmCount do
|
||||||
zoneSpawns[toRemove[i]] = nil
|
local key = toRemove[i]
|
||||||
|
if type(key) == "number" then
|
||||||
|
table.remove(zoneSpawns, key)
|
||||||
|
else
|
||||||
|
zoneSpawns[key] = nil
|
||||||
|
end
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
if changed then
|
if changed then
|
||||||
|
|||||||
Reference in New Issue
Block a user