WENOPAL

L05 · Tablas

Listas, mapas y conjuntos con tablas de Lua, cómo las recorre Kahlua, la librería table y la diferencia entre referencia, copia y copia profunda.

Curso técnico · Build 42 · Actualizado el

Objetivo: usar tablas como listas, mapas y conjuntos, recorrerlas sin sorpresas en Kahlua y entender cuándo dos variables apuntan a la misma tabla. Al final vas a leer las listas de loot del juego como las lee el propio juego.

1. Una sola estructura para todo

Lua tiene un solo tipo para agrupar datos: la tabla. Con ella se hacen listas, diccionarios, conjuntos, objetos y módulos. Se crea con {}:

lua
local vacia = {}

local zombi = {
    nombre = "Corredor",
    vida = 1.5,
    rapido = true,
}

print(zombi.nombre)        -- Corredor
print(zombi["vida"])       -- 1.5: t.x es lo mismo que t["x"]
print(zombi.color)         -- nil: una clave que no existe vale nil

Agregar, cambiar y borrar es lo mismo: asignar.

lua
zombi.color = "verde"      -- agrega
zombi.vida = 2             -- cambia
zombi.rapido = nil         -- borra (la clave deja de existir)

Las claves pueden ser strings, números, booleanos, otras tablas, funciones u objetos del juego. Lo único que no puede ser clave es nil. En un constructor, una clave que resulta nil se descarta sin error:

lua
local x = nil
local t = { [x] = 1 }      -- no da error, pero t queda vacía

Cuidado con los tipos: el número 1 y el string "1" son claves distintas.

lua
local t = {}
t[1] = "número"
t["1"] = "texto"
print(t[1], t["1"])        -- número  texto

Esto muerde cuando un dato llega como texto (por ejemplo, de un archivo o de un comando) y lo usas como índice. Conviértelo antes con tonumber (L02).

2. Listas: empiezan en 1

Una lista es una tabla con claves 1, 2, 3... sin saltos:

lua
local frutas = { "Apple", "Banana", "Cherry" }

print(frutas[1])    -- Apple (no hay frutas[0])
print(#frutas)      -- 3: el operador # da el largo
print(frutas[#frutas])  -- Cherry: el último

En Lua las listas empiezan en 1, no en 0. En L06 vas a ver que las listas de Java que te da el juego (ArrayList) empiezan en 0: son cosas distintas, cada una con su regla.

Para recorrer una lista con un for numérico:

lua
for i = 1, #frutas do
    print(i, frutas[i])
end

Para agregar al final, dos formas equivalentes:

lua
frutas[#frutas + 1] = "Grapes"
table.insert(frutas, "Lemon")

3. # y los huecos

En Kahlua, #t no cuenta los elementos uno por uno: hace una búsqueda binaria sobre las claves enteras. Si la lista no tiene huecos, el resultado es correcto. Si tiene un nil en el medio, el resultado depende de cómo quedó la tabla por dentro:

lua
print(#{ 1, 2, nil, 4 })                -- 2 en Kahlua
print(#{ 1, 2, 3, nil, 5, 6, 7, 8 })    -- 8 en Kahlua

local t = { "a", "b", "c", "d", "e" }
t[2] = nil
print(#t)                               -- 5 en Kahlua, aunque t[2] es nil

No memorices esos números: lo que importa es que con huecos # no es confiable, y todo lo que se apoya en # (table.insert, table.remove, table.concat, unpack) tampoco. Reglas para no tener problemas:

  • No asignes nil a una posición del medio de una lista. Para sacar un elemento usa table.remove(t, i), que corre los siguientes un lugar.
  • Si necesitas guardar "nada" en una posición, usa un valor centinela como false.
  • Si una lista puede tener huecos (por ejemplo, la armaste con {...} de argumentos que pueden ser nil, como viste en L04), guarda el largo aparte: { n = select('#', ...), ... }.

4. Recorrer: ipairs y pairs

ipairs(t)pairs(t)
Qué recorret[1], t[2], ... hasta el primer nilTodas las claves, de cualquier tipo
Orden1, 2, 3...En Kahlua, orden de inserción (ver abajo)
Úsalo paraListasMapas, conjuntos, tablas mezcladas
lua
local lista = { "Apple", "Banana", nil, "Grapes" }
for i, v in ipairs(lista) do
    print(i, v)          -- 1 Apple, 2 Banana, y se detiene en el hueco
end

local precios = { Apple = 2, Banana = 3, Cherry = 1 }
for clave, valor in pairs(precios) do
    print(clave, valor)
end

Si no necesitas la clave o el valor, se usa _ como nombre (es una variable normal, solo es una convención para decir "no me importa"):

lua
for _, v in ipairs(lista) do print(v) end

El orden de pairs en Kahlua

En Lua estándar el orden de pairs no está definido. En Kahlua cada tabla es por dentro un LinkedHashMap de Java, así que pairs recorre las claves en el orden en que se insertaron:

lua
local t = {}
t.zeta = 1
t.alfa = 2
t.medio = 3
for k in pairs(t) do print(k) end    -- zeta, alfa, medio

t.alfa = 20                           -- cambiar el valor no la mueve
t.zeta = nil                          -- borrar la saca
t.zeta = 9                            -- volver a crearla la pone al final
for k in pairs(t) do print(k) end    -- alfa, medio, zeta

Es útil para depurar (los volcados salen en un orden predecible), pero no dependas de él para nada que tenga que coincidir entre dos máquinas, como el servidor y un cliente: el orden depende de en qué orden se insertó en cada una, y eso puede cambiar por el orden de carga de los mods o por cómo llegaron los datos. Si necesitas un orden, pon las claves en una lista y ordénala con table.sort (sección 5).

Borrar mientras recorres

El iterador de pairs de Kahlua trabaja sobre una copia de las claves tomada al empezar. Por eso:

  • Borrar la entrada actual (t[k] = nil) dentro del bucle es seguro.
  • Las claves que agregues durante el bucle no aparecen en ese recorrido.
  • Si borras otra clave que todavía no salió, igual va a salir, pero con valor nil.
lua
local pesos = { Apple = 2, Watermelon = 0.5, Pineapple = 1, Avocado = 1 }
for item, peso in pairs(pesos) do
    if peso < 1 then
        pesos[item] = nil        -- seguro con pairs en Kahlua
    end
end

Con una lista es distinto: table.remove corre los elementos, y si recorres hacia adelante te saltas el que quedó en el lugar del borrado. Recorre hacia atrás:

lua
local items = { "Apple", "Rotten", "Rotten", "Banana" }
for i = #items, 1, -1 do         -- de atrás hacia adelante
    if items[i] == "Rotten" then
        table.remove(items, i)
    end
end
-- items = { "Apple", "Banana" }

5. La librería table

FunciónQué hace
table.insert(t, v)Agrega v al final
table.insert(t, i, v)Inserta v en la posición i y corre los siguientes
table.remove(t)Saca el último y lo devuelve
table.remove(t, i)Saca el de la posición i, corre los siguientes y lo devuelve
table.sort(t) / table.sort(t, menor)Ordena la lista en el lugar
table.concat(t, sep, i, j)Une los elementos en un string
table.wipe(t)Extra de Kahlua: vacía la tabla (la misma tabla, no una nueva)
table.isempty(t)Extra de Kahlua: true si la tabla no tiene ninguna clave
lua
local cola = { "Apple", "Banana" }
table.insert(cola, 1, "Cherry")      -- { "Cherry", "Apple", "Banana" }
local primero = table.remove(cola, 1) -- primero = "Cherry"
local ultimo = table.remove(cola)     -- ultimo = "Banana", cola = { "Apple" }

table.sort

Sin segundo argumento ordena de menor a mayor (números o strings, no los mezcles). Con una función de comparación decides tú: la función recibe dos elementos y devuelve true si el primero tiene que ir antes que el segundo.

lua
local nums = { 5, 2, 8, 1 }
table.sort(nums)                                     -- { 1, 2, 5, 8 }
table.sort(nums, function(a, b) return a > b end)    -- { 8, 5, 2, 1 }

local loot = {
    { nombre = "Apple", peso = 2 },
    { nombre = "Watermelon", peso = 0.5 },
    { nombre = "Pineapple", peso = 1 },
}
table.sort(loot, function(a, b) return a.peso > b.peso end)
-- Apple, Pineapple, Watermelon

La función tiene que ser un "menor estricto": nunca devuelvas true para dos elementos iguales (no uses >= ni <=).

table.concat

Une una lista de strings o números. Es la forma correcta de armar un texto con muchas partes (concatenar con .. dentro de un bucle crea un string nuevo en cada vuelta; L10 vuelve sobre esto):

lua
local partes = { "Apple", "Banana", "Cherry" }
print(table.concat(partes, ", "))      -- Apple, Banana, Cherry
print(table.concat(partes, ", ", 2))   -- Banana, Cherry

Una rareza de Kahlua: si un elemento no es string ni número, table.concat no da error, escribe null. table.concat({ "a", true, "b" }, ",") da "a,null,b". Si ves null en un texto tuyo, busca un table.concat con un booleano o una tabla adentro, y pásalos por tostring antes.

table.wipe y table.isempty

Son extras de Kahlua que no existen en Lua estándar, y el juego los usa mucho (table.wipe aparece 75 veces):

lua
local seleccion = { "Apple", "Banana" }
table.wipe(seleccion)
print(#seleccion, table.isempty(seleccion))   -- 0  true

local mapa = { Apple = 2 }
print(#mapa)                 -- 0: # solo mira la parte de lista
print(table.isempty(mapa))   -- false: isempty mira todas las claves

#t == 0 no sirve para saber si un mapa está vacío; table.isempty(t) sí.

6. Mapas y conjuntos

Un mapa (o diccionario) asocia una clave con un valor. Es la tabla que más vas a usar en un mod: datos por tipo de ítem, por jugador, por nombre de zona.

lua
local calorias = {
    ["Base.Apple"] = 95,
    ["Base.Banana"] = 105,
}

calorias["Base.Cherry"] = 50

local tipo = "Base.Apple"
if calorias[tipo] then
    print("[CursoLab] " .. tipo .. " tiene " .. calorias[tipo] .. " calorías")
end

Buscar en un mapa por clave es instantáneo; buscar un valor recorriendo una lista es más lento cuanto más larga es la lista. Si vas a preguntar muchas veces "¿está X?", arma un mapa una vez y pregunta muchas.

Para contar cosas, el patrón es t[k] = (t[k] or 0) + 1:

lua
local muertes = {}
local function anotar(tipoZombi)
    muertes[tipoZombi] = (muertes[tipoZombi] or 0) + 1
end

Si el valor de un mapa puede ser false, revisa con ~= nil en vez de if t[k] then, por la misma razón que en L04 con los booleanos por defecto.

Un conjunto es un mapa donde solo importa la clave: el valor es true.

lua
local comestibles = { Apple = true, Banana = true, Cherry = true }

if comestibles["Apple"] then print("se come") end
comestibles["Cherry"] = nil       -- sacar del conjunto
lua
local function removeDuplicates(list)
    local result = {}
    local seen = {}
    for _,item in ipairs(list) do
        if not seen[item] then
            seen[item] = true
            table.insert(result, item)
        end
    end
    return result
end

Compáralo con luautils.tableContains (en shared/luautils.lua), que recorre toda la tabla con pairs cada vez que preguntas. Para una pregunta suelta está bien; dentro de un bucle, el conjunto gana.

7. Tablas anidadas

Una tabla puede tener tablas adentro. Así están armados casi todos los datos del juego:

lua
local partida = {
    jugadores = {
        { nombre = "Kate", kills = 12 },
        { nombre = "Bob", kills = 3 },
    },
    config = { dificultad = "dura" },
}

print(partida.jugadores[1].nombre)       -- Kate
print(partida.config.dificultad)         -- dura

Si una tabla del medio no existe, el acceso falla con un error del tipo attempted index ... of non-table. Para leer datos que pueden faltar, revisa cada nivel con and:

lua
local dif = partida.config and partida.config.dificultad or "normal"

Míralo en el juego: las listas de loot

Abre server/Items/ProceduralDistributions.lua. Es una sola tabla global gigante. La entrada de los refrigeradores comunes empieza así (línea 22287):

lua
	FridgeGeneric = {
		ignoreZombieDensity = true,
		rolls = 2,
		items = {
			-- Fruit
			"Apple", 2,
			"Banana", 2,
			"Cherry", 2,
			"Grapefruit", 2,
			"Grapes", 2,
			"Lemon", 2,
			"Lime", 2,
			"Orange", 2,
			"Peach", 2,
			"Pear", 2,
			"Pineapple", 1,
			"Strewberrie", 2,
			"Watermelon", 0.5,

Tres niveles: ProceduralDistributions.list es un mapa de contenedores; cada contenedor es un mapa con rolls, items y a veces junk (que tiene su propio rolls e items); e items es una lista plana que alterna nombre y peso: nombre, peso, nombre, peso.... El ítem está en las posiciones impares y su peso en la siguiente. Para leerla se recorre de a dos:

lua
local function listarLoot(nombreContenedor)
    local cont = ProceduralDistributions.list[nombreContenedor]
    if not cont or not cont.items then
        print("[CursoLab] No existe el contenedor " .. tostring(nombreContenedor))
        return
    end
    local items = cont.items
    for i = 1, #items, 2 do
        local nombre, peso = items[i], items[i + 1]
        print("[CursoLab] " .. nombre .. " peso " .. tostring(peso))
    end
end

listarLoot("FridgeGeneric")

Por eso, para agregar un ítem, el módulo 13 hace dos table.insert seguidos (primero el nombre, después el peso). Si alguien inserta solo uno, toda la lista queda corrida: los nombres pasan a las posiciones pares y el juego lee pesos donde había nombres.

Leer estas tablas se puede en cualquier momento de una partida de un jugador. Cambiarlas para que el loot cambie de verdad tiene su momento exacto (OnPostDistributionMerge): eso es el módulo 13.

Recorrer algo anidado: recursión

Para recorrer tablas que tienen tablas adentro, sin saber cuántos niveles hay, se usa una función recursiva (L04). Así junta el juego las distribuciones de los mods con las suyas, en server/Items/SuburbsDistributions.lua:

lua
function MergeDistributionRecursive(_orig, _mod)
	for k,v in pairs(_mod) do
		if _orig[k]~=nil then
			if type(_mod[k])=="table" then
				if type(k)=="string" and k=="items" then
					for _,v2 in ipairs(_mod[k]) do
						table.insert(_orig[k], v2)
					end
				else
					MergeDistributionRecursive(_orig[k], _mod[k])
				end
			end
		else --if original doesnt have the table the mod has, add it.
			_orig[k] = _mod[k]
		end
	end
end

Léela con lo que ya sabes: si la clave no existe en el original, se copia la referencia (_orig[k] = _mod[k]); si existe y es la lista items, agrega los pares al final con ipairs y table.insert; si existe y es otra tabla, baja un nivel llamándose a sí misma.

8. Referencias, copias superficiales y copias profundas

Esta es la parte que más errores causa. Una variable no contiene la tabla: contiene una referencia a ella. Asignar una tabla a otra variable no la copia:

lua
local a = { vida = 100 }
local b = a          -- b apunta a LA MISMA tabla
b.vida = 50
print(a.vida)        -- 50
print(a == b)        -- true: misma tabla

print({} == {})      -- false: dos tablas distintas, aunque estén vacías

Lo mismo pasa al pasar una tabla a una función: la función recibe la referencia y puede cambiar la tabla original.

lua
local function vaciar(t)
    table.wipe(t)    -- vacía la tabla del que llamó
end

local function reemplazar(t)
    t = {}           -- solo cambia la variable local t; la original no se toca
end

Copia superficial

Una tabla nueva con las mismas claves y valores. Si un valor es otra tabla, se copia la referencia a esa tabla interna, no la tabla:

lua
local function copiaSuperficial(t)
    local nueva = {}
    for k, v in pairs(t) do
        nueva[k] = v
    end
    return nueva
end

local original = { nombre = "Kate", stats = { fuerza = 5 } }
local copia = copiaSuperficial(original)
copia.nombre = "Bob"          -- no afecta al original
copia.stats.fuerza = 10       -- SÍ afecta: stats es la misma tabla en las dos
print(original.nombre, original.stats.fuerza)   -- Kate  10

Copia profunda

Copia también las tablas de adentro, con recursión:

lua
local function copiaProfunda(t)
    if type(t) ~= "table" then return t end
    local nueva = {}
    for k, v in pairs(t) do
        nueva[k] = copiaProfunda(v)
    end
    return nueva
end

El juego ya trae una: la función global copyTable(t), definida en Java (LuaManager.copyTable) y usada en 49 lugares del Lua del juego. Según su código, copia recursivamente las tablas de adentro, copia las claves tal cual y no copia la metatabla. Tampoco controla ciclos: no se la pases a una tabla que se contiene a sí misma (directa o indirectamente), porque se llamaría sin fin.

lua
local copia = copyTable(original)
copia.stats.fuerza = 99
print(original.stats.fuerza)   -- no cambia

Una versión que sí aguanta ciclos y además conserva la metatabla está en client/PZAPI/ui/atoms/Meta.lua. Usa una tabla seen para recordar lo que ya copió:

lua
UI._copyValue = function(obj, seen)
    if type(obj) ~= 'table' then return obj end
    if seen and seen[obj] then return seen[obj] end
    local s = seen or {}
    local res = setmetatable({}, getmetatable(obj))
    s[obj] = res
    for k, v in pairs(obj) do
        if k ~= "javaObj" then
            res[UI._copyValue(k, s)] = UI._copyValue(v, s)
        end
    end
    for i, v in ipairs(obj) do res[i] = UI._copyValue(v, s) end
    return res
end

Fíjate en el if k ~= "javaObj": los objetos de Java no se pueden copiar así, solo se puede copiar la referencia. Lo ves en L06.

Cuándo copiar

SituaciónQué hacer
Quieres que dos partes del código vean los mismos datosComparte la referencia
Vas a modificar una tabla que te pasaron y el dueño no debe enterarseCopia (superficial si no tiene tablas adentro, profunda si tiene)
Quieres vaciar una tabla que otros tienen guardadatable.wipe(t)
Quieres empezar de cero sin tocar lo que otros tienent = {}

Ejercicios

Prueba todo en tu archivo de CursoLab o en la consola de debug, dentro de una partida de un jugador (L01).

Ejercicio L5.1 · La lista de la mochila

Escribe CursoLab.mochila, una lista vacía, y tres funciones:

  • CursoLab.guardar(item): lo agrega al final.
  • CursoLab.sacar(item): saca la primera aparición de ese ítem y devuelve true, o false si no estaba.
  • CursoLab.verMochila(): imprime los ítems ordenados alfabéticamente y separados por coma, sin cambiar el orden de la mochila.

Listo cuando: después de guardar "Banana", "Apple", "Banana" y sacar "Banana", verMochila() imprime [CursoLab] Apple, Banana y CursoLab.mochila[1] sigue siendo "Apple".

Solución
lua
CursoLab = CursoLab or {}
CursoLab.mochila = CursoLab.mochila or {}

function CursoLab.guardar(item)
    table.insert(CursoLab.mochila, item)
end

function CursoLab.sacar(item)
    local m = CursoLab.mochila
    for i = 1, #m do
        if m[i] == item then
            table.remove(m, i)
            return true
        end
    end
    return false
end

function CursoLab.verMochila()
    local copia = {}
    for i, v in ipairs(CursoLab.mochila) do copia[i] = v end   -- copia para no desordenar
    table.sort(copia)
    print("[CursoLab] " .. table.concat(copia, ", "))
end

Como sacar devuelve apenas encuentra, el table.remove dentro del for no causa saltos.

Ejercicio L5.2 · Los pesos de un contenedor

Escribe CursoLab.topLoot(contenedor, n) que lea ProceduralDistributions.list[contenedor].items, arme una lista de tablas { nombre = ..., peso = ... }, la ordene de mayor a menor peso e imprima los n primeros (por defecto 5).

Listo cuando: CursoLab.topLoot("KitchenCannedFood", 3) imprime TinOpener primero (peso 8) y CursoLab.topLoot("NoExiste") imprime un aviso en vez de un error.

Solución
lua
function CursoLab.topLoot(contenedor, n)
    n = n or 5
    local cont = ProceduralDistributions.list[contenedor]
    if not cont or not cont.items then
        print("[CursoLab] No existe: " .. tostring(contenedor))
        return
    end
    local lista = {}
    local items = cont.items
    for i = 1, #items, 2 do
        table.insert(lista, { nombre = items[i], peso = items[i + 1] })
    end
    table.sort(lista, function(a, b) return a.peso > b.peso end)
    for i = 1, math.min(n, #lista) do
        print("[CursoLab] " .. i .. ". " .. lista[i].nombre .. " (" .. lista[i].peso .. ")")
    end
end

Ejercicio L5.3 · Lo que tienen en común

Escribe CursoLab.enComun(contA, contB) que devuelva una lista con los ítems que aparecen en los dos contenedores, usando un conjunto para el primero (no dos bucles anidados). Ignora los pesos.

Listo cuando: CursoLab.enComun("FridgeGeneric", "FridgeOther") devuelve una lista con frutas como "Apple", y la imprimes con table.concat.

Solución
lua
local function nombresDe(contenedor)
    local cont = ProceduralDistributions.list[contenedor]
    local nombres = {}
    if not cont or not cont.items then return nombres end
    for i = 1, #cont.items, 2 do
        table.insert(nombres, cont.items[i])
    end
    return nombres
end

function CursoLab.enComun(contA, contB)
    local enA = {}
    for _, nombre in ipairs(nombresDe(contA)) do
        enA[nombre] = true
    end
    local resultado, yaPuesto = {}, {}
    for _, nombre in ipairs(nombresDe(contB)) do
        if enA[nombre] and not yaPuesto[nombre] then
            yaPuesto[nombre] = true
            table.insert(resultado, nombre)
        end
    end
    return resultado
end

print("[CursoLab] " .. table.concat(CursoLab.enComun("FridgeGeneric", "FridgeOther"), ", "))

yaPuesto evita repetidos si un ítem aparece dos veces en la misma lista.

Ejercicio L5.4 · Referencia o copia

Sin ejecutarlo, escribe qué imprime cada print. Después ejecútalo y compara.

lua
local base = { nombre = "Kate", stats = { fuerza = 5 }, tags = { "rapida" } }

local r = base
local s = {}
for k, v in pairs(base) do s[k] = v end
local p = copyTable(base)

r.nombre = "Bob"
s.stats.fuerza = 7
p.stats.fuerza = 9
table.insert(s.tags, "fuerte")
table.wipe(p.tags)

print(base.nombre, base.stats.fuerza, #base.tags)
print(s.nombre, #s.tags, #p.tags)

Listo cuando: acertaste las dos líneas y puedes explicar cada valor diciendo "es la misma tabla" o "es otra tabla".

Solución
Bob   7   2
Kate  2   0
  • r es la misma tabla que base, así que base.nombre pasa a "Bob".
  • s es una copia superficial: tiene su propio nombre (sigue "Kate", se copió antes del cambio), pero s.stats y s.tags son las mismas tablas que en base. Por eso base.stats.fuerza queda en 7 y base.tags tiene 2 elementos.
  • p es una copia profunda: cambiar p.stats y vaciar p.tags no toca a base.

Ejercicio L5.5 · Limpiar un mapa

Arma un mapa nombre → peso con los ítems de FridgeGeneric (si un ítem aparece dos veces, suma los pesos). Después, en un solo recorrido con pairs, borra todos los que tienen peso menor que 1 y cuenta cuántos borraste. Al final imprime cuántos quedan, contándolos con pairs (no con #).

Listo cuando: imprime algo como [CursoLab] Borrados: X, quedan: Y y explicas por qué #mapa daría 0.

Solución
lua
local cont = ProceduralDistributions.list.FridgeGeneric
local mapa = {}
for i = 1, #cont.items, 2 do
    local nombre, peso = cont.items[i], cont.items[i + 1]
    mapa[nombre] = (mapa[nombre] or 0) + peso
end

local borrados = 0
for nombre, peso in pairs(mapa) do
    if peso < 1 then
        mapa[nombre] = nil        -- seguro: pairs de Kahlua recorre una copia de las claves
        borrados = borrados + 1
    end
end

local quedan = 0
for _ in pairs(mapa) do quedan = quedan + 1 end

print("[CursoLab] Borrados: " .. borrados .. ", quedan: " .. quedan)

#mapa da 0 porque # solo mira las claves enteras 1, 2, 3... y este mapa solo tiene claves de texto.

Desafío L5

Escribe CursoLab.dondeSale(item): recorre todos los contenedores de ProceduralDistributions.list y devuelve una lista de { contenedor = ..., peso = ..., junk = true/false } con cada lugar donde aparece ese ítem, mirando tanto items como junk.items. Ordénala por peso de mayor a menor y escribe CursoLab.imprimirDondeSale(item, n) que muestre los n primeros (por defecto 10) en una sola línea armada con table.concat.

Condiciones:

  • No modifiques ProceduralDistributions: solo léelo.
  • Tolera contenedores sin items o sin junk.
  • Si el ítem no aparece en ningún lado, dilo.

Listo cuando: CursoLab.imprimirDondeSale("Apple") empieza por los contenedores donde más salen manzanas (peso 20, como GroceryStandFruits1 o ProduceStorageLooseFruit) y CursoLab.imprimirDondeSale("Sopaipilla") dice que no aparece.

Una solución
lua
local function buscarEn(lista, item, contenedor, esJunk, resultado)
    if not lista then return end
    for i = 1, #lista, 2 do
        if lista[i] == item then
            table.insert(resultado, { contenedor = contenedor, peso = lista[i + 1], junk = esJunk })
        end
    end
end

function CursoLab.dondeSale(item)
    local resultado = {}
    for nombre, cont in pairs(ProceduralDistributions.list) do
        if type(cont) == "table" then
            buscarEn(cont.items, item, nombre, false, resultado)
            if cont.junk then
                buscarEn(cont.junk.items, item, nombre, true, resultado)
            end
        end
    end
    table.sort(resultado, function(a, b) return a.peso > b.peso end)
    return resultado
end

function CursoLab.imprimirDondeSale(item, n)
    n = n or 10
    local lugares = CursoLab.dondeSale(item)
    if #lugares == 0 then
        print("[CursoLab] " .. tostring(item) .. " no aparece en ProceduralDistributions")
        return
    end
    local partes = {}
    for i = 1, math.min(n, #lugares) do
        local l = lugares[i]
        local texto = l.contenedor .. " " .. tostring(l.peso)
        if l.junk then texto = texto .. " (junk)" end
        table.insert(partes, texto)
    end
    print("[CursoLab] " .. item .. ": " .. table.concat(partes, ", "))
end

Fíjate en el tostring(l.peso): si algún día un mod rompe una lista y deja un nil como peso, .. daría error, pero tostring escribe nil y te muestra el problema.


Anterior: L04 · Funciones · Siguiente: L06 · Lua y objetos de Java