--[[ Legend of Grimrock debugging toolkit! This stashes everything in a global named 'd', to avoid polluting the global namespace. You can run d.globalize() to copy things into the global namespace, and it might not even break anything. At least one of these functions does modify in-game objects; THIS MAY NOT WORK. This is provided without warranty of any sort, but free of charge. You're welcome to mess around with it. Quick summary: 1. Copy this file into your Grimrock directory under the name 'debug.lua'. 2. Get a replacement 'Console.lua' file and store it in your Grimrock program directory. I found one reading the forums, you might be able to find one elsewhere or just extract the file. 3. Modify that file so you can start the console; this mostly means making sure the character at 0x7c1 is something you can type (I used ~, the default is a symbol my keyboard hasn't got.) 4. You might want to change the maximum lines for the console; look for an 0x0A at location 0x50. You can set it pretty high but it will still cap around 40. 5. Log in, hit ~, and you should see a grey line with a '$' prompt at the top of the screen. 6. Type dofile('debug.lua') and hit return. You should get a message to the effect of "debug tools loaded". Commands are all implemented as functions you can call. A few examples: d.who() Lists party state. d.showfood() Lists food items currently carried. d.showlight() Lists total fuel remaining on your carried torches. d.bump_level(char, [levels]) Levels a character (once, by default). d.bump_stat(char, stat, amount) Increases a stat. Amount may be negative. NOTE: Willpower and vitality effects on existing health and energy do not occur. They affect future level gains, but not your current stats. I have no idea whether that's a bug. d.speed(multiplier) Sets the game speed to a given multiplier; 1 is the default speed. d.bump_skill(char, skill, amount) Increases a skill. Amount CANNOT be negative. There is no unlearn. d.feed(char) Refills character's food bar completely. d.heal(char) Restores energy and health. d.cure(char) Dispels a number of likely-unwanted conditions. d.beacon(name) Mark your current location... d.goback(name) Return to a marked location. Might not work! d.levelchart() Prints out a level chart BY LEVELLING TO 50. Do not do this if you would regret the destruction of your game. d.level_calc() Uses levelchart() -- AGAIN DO NOT DO THIS -- and shows you that levels sort of even out to a consistent 1.370x multiplier for XP at each successive level. d.contains(pattern) Lists things in _G which contain something matching the given pattern. d.grep(pattern) Lists things in _G which match the given pattern. d.iterate() If you are likely to get any benefit from using this, you should be able to understand it from the code samples. Not gonna try to explain this one. d.dump(table [, depth]) Dumps a table, optionally capping at a given depth. d.showkeys(table) Shows the keys of the table in sorted order and many to a line. d.partymembers() Returns the party members as an array. d.all_items(char) Returns a list of items held by a character. WARNING: This modifies some items (adding a "contained_in" field). d.reloadui() Re-loads debug.lua. d.loc() Prints current location. d.hotkeys() Toggle developer mode (and hotkeys). ]]-- d = {} d.stashed_locations = d.stashed_locations or {} function d.whereami() for k, v in pairs(map.nodeToEntity) do if v.champions and #v.champions > 0 then return { x = v.x, y = v.y, level = v.level } end end return nil end d.facings = { 'north', 'east', 'south', 'west' } function d.loc() local where = d.whereami() if where then d.printf("At %d, %d facing %s.", where.x, where.y, d.facings[where.level] or 'unknown') else d.printf("I'm trying to find myself. Have you seen me anywhere?") end end function d.beacon(name) local where = d.whereami() if where then d.printf("Found party at %d, %d, facing %s, stashing as '%s'.", where.x, where.y, d.facings[where.level] or 'unknown', name and tostring(name) or 'default') d.stashed_locations[name or 'default'] = where return end end function d.goback(name) local loc = d.stashed_locations[name or 'default'] if loc then d.printf("Returning party to %d, %, facing %s.", loc.x, loc.y, d.facings[loc.level] or 'unknown') teleport(loc.x, loc.y, loc.level) else if name then d.printf("No stashed location named '%s'; use d.beacon() first to set a location.", tostring(name)) else d.printf("No default stashed location; use d.beacon() first to set a location.") end d.printf("Known locations:") d.showkeys(d.stashed_locations) end end function d.reloadui() dofile('debug.lua') end function d.sprintf(fmt, ...) local foo = function(...) return string.format(fmt or 'nil', ...) end local status, value = pcall(foo, ...) if status then return value else return 'Format "' .. (fmt or 'nil') .. '": ' .. value end end function d.printf(fmt, ...) print(d.sprintf(fmt, ...)) end function d.fprintf(f, fmt, ...) f:write(d.sprintf(fmt, ...)) end function d.to_s(v) local foo = function() return tostring(v) end okay, values = pcall(foo) if okay then return values else return '[invalid ' .. type(v) .. ']' end end function d.keys(t) local kt = {} for k, v in pairs(t) do table.insert(kt, d.to_s(k)) end return kt end d.level_requirements = level_requirements or {} d.level_requirements[1] = 0 function d.levelchart() local c = d.partymembers() if c and c[1] then while c[1].class.level < 50 do d.level_requirements[c[1].class.level + 1] = c[1].class.nextLevel d.printf("%2d: %.0f", c[1].class.level + 1, c[1].class.nextLevel) c[1]:gainExp(c[1].class.nextLevel - c[1].class.exp) end end end function d.level_calc() if not d.level_requirements[10] then d.levelchart() end local prev = nil for level, exp in ipairs(d.level_requirements) do if prev and prev > 0 then d.printf("%d: %.3f", level, exp / prev) end prev = exp end end function d.partymembers() local champions = {} for k, v in pairs(map.nodeToEntity) do if v.champions and #v.champions > 0 then for idx, char in ipairs(v.champions) do champions[char.championIndex] = char end end end return champions end function d.capitalize_one(str) if not str or type(str) ~= 'string' then return 'nil' else return string.upper(string.sub(str, 1, 1)) .. string.lower(string.sub(str, 2)) end end function d.capitalize(str) if not str or type(str) ~= 'string' then return 'nil' end local words = {} while #str > 0 do before, after = string.match(str, '([^_]*)_(.*)') if after then table.insert(words, d.capitalize_one(before)) str = after else table.insert(words, d.capitalize_one(str)) str = '' end end return table.concat(words, ' ') end d.conditions_without_values = { overloaded = true, burdened = true } function d.show_char(char) if type(char) ~= 'table' then char = d.find_champ(char) end if not char then return end local prettyskill = '' if char.skillPoints > 0 then prettyskill = d.sprintf('[%d skill point%s]', char.skillPoints, char.skillPoints == 1 and '' or 's') end d.printf("%s: Level %d %s %s %s", char.name, char.class.level, char.race.name, char.class.name, prettyskill) local out = '' local t = {} for k, talent in pairs(char.talents) do if talent.trait then table.insert(t, talent.uiName) end end d.printf(" Traits: %s", table.concat(t, ', ')) local fed = '' if char.food <= 250 then fed = 'Starving' elseif char.food <= 350 then fed = 'Hungry' else fed = 'Well-fed' end d.printf(" Food: %4d/1000 (%s)", char.food, fed) d.printf(" XP: %.0f / %.0f", char.class.exp, char.class.nextLevel) d.printf(" Health: %4d/%-4d Energy: %4d/%-4d", char.stats.health.value, char.stats.health.max, char.stats.energy.value, char.stats.energy.max) d.printf(" Strength: %3d/%-3d Dexterity: %3d/%-3d", char.stats.strength.value, char.stats.strength.max, char.stats.dexterity.value, char.stats.dexterity.max) d.printf(" Vitality: %3d/%-3d Willpower: %3d/%-3d", char.stats.vitality.value, char.stats.vitality.max, char.stats.willpower.value, char.stats.willpower.max) for idx, skill in ipairs(char.skills) do out = out .. d.sprintf(" %16s: %2d", d.capitalize(skill.name), skill.level) if idx % 3 == 0 then print(out) out = '' end end if char.conditions then for idx, details in pairs(char.conditions) do if details.value > 0 then if d.conditions_without_values[details.name] then d.printf(" %16s: %s", d.capitalize(details.uiName or details.name), details.description or '(No description available)') elseif details.name == 'unused_skill_points' then -- omit this, since we already handled it above. else d.printf(" %16s: %.1f %s", d.capitalize(details.uiName or details.name), details.value, details.description or '(No description available)') end end end end end function d.who() local p = d.partymembers() local spacer = false for idx, char in ipairs(p) do if spacer then print('') else spacer = true end d.show_char(char) end end function d.find_champ(idx_or_name) local p = d.partymembers() if not idx_or_name then d.printf("No character specified. Remember that names need to be quoted.") return end local n = string.lower(idx_or_name or 'nil') for idx, char in ipairs(p) do if idx == idx_or_name or string.find(string.lower(char.name), n) then return char end end d.printf("Couldn't find champion '%s'. Use index (1-4) or name.", tostring(idx_or_name)) return nil end function d.bump_skill(char, skill, value) if not skill then d.printf("No skill specified. Remember that names need to be quoted.") return end local c = d.find_champ(char) if c then if not c.skills then d.printf("Inexplicably, this character has no skills?") return end for idx, s in ipairs(c.skills) do if s.name == skill then c:trainSkill(skill, value) d.printf("Added %d to %s.", value or 1, skill) d.show_char(c) return end end d.printf("No skill '%s'. Try one of:", skill) local t = {} for idx, s in ipairs(c.skills) do table.insert(t, s.name) end printf(" %s", table.concat(t, ', ')) end end function d.bump_stat(char, stat, value) if not stat then d.printf("No stat specified. Remember that names need to be quoted.") return end local c = d.find_champ(char) if c then if c.stats and c.stats[stat] then c:setStatMax(stat, c.stats[stat].max + math.floor(value)) c:setStat(stat, c.stats[stat].value + math.floor(value)) d.printf("Added %d to %s.", value, stat) d.show_char(c) else d.printf("No stat '%s'. Try one of:", stat) if c.stats then d.showkeys(c.stats) end end end end function d.feed(char) local c = d.find_champ(char) if c then c.food = 1000 d.printf("Wow, this cheating is DELICIOUS.") end end function d.heal(char) local c = d.find_champ(char) if c then c.stats.energy.value = c.stats.energy.max c.stats.health.value = c.stats.health.max d.printf("I feel much better.") end end function d.condition_set(char, condition, value) if char and char.conditions and char.conditions[condition] then local c = char.conditions[condition] c.value = value if c.timer and c.timer > 1 then c.timer = 0.1 end end end d.bad_conditions = { 'blind', 'cursed', 'diseased', 'paralyzed', 'poison', 'slow' } function d.cure(char) local c = d.find_champ(char) if c then if c.conditions then for _, cond in ipairs(d.bad_conditions) do d.condition_set(c, cond, 0) end d.printf("Wow, these vitamins really *are* magical.") end end end function d.bump_level(char, levels) levels = levels or 1 local c = d.find_champ(char) if c then while levels > 1 and c.class.level < 50 do local needed = c.class.nextLevel - c.class.exp d.printf("Giving %d points to %s (now level %d).", needed, c.name, c.class.level + 1) c:gainExp(needed) levels = levels - 1 end d.show_char(c) end end function d.showkeys(t) t = t or _G if type(t) ~= 'table' then d.printf("showkeys needs a table.") return end local kt = d.keys(t) table.sort(kt) local out = {} for _, k in ipairs(kt) do table.insert(out, k) if #out > 15 then print(table.concat(out, ', ')) out = {} end end if #out > 0 then print(table.concat(out, ', ')) end end function d.extract_name(v) return v.uiName or v.name or v.description end function d.guess_name(v) status, name = pcall(d.extract_name, v) if status then return name else return false end end function d.print_item(prefix, key, value, t, k, v) local name = '' if type(v) == 'table' then name = d.guess_name(v) if name then name = d.sprintf(' (%s)', name) else name = '' end end printf("%s%s: %s%s", prefix, key, value, name) end function d.print_if(pattern, prefix, key, value) if string.match(value, pattern) then printf("%s%s: %s", prefix, key, value) end end function d.contains(pattern, depth, t) t = t or _G local tables = {} local keys = {} d.iterate(t, depth, function(p, ks, vs, t, k, v) if string.match(vs, pattern) then table.insert(tables, t); table.insert(keys, k) end end) return tables, keys end function d.grep(pattern, depth, t) t = t or _G d.iterate(t, depth, function(p, k, v) d.print_if(pattern, p, k, v) end) end function d.all_items(char) local all_items = {} for _, item in pairs(char.items) do table.insert(all_items, item) if item.container then -- ignore nested containers since the game seems not to allow them for _, another in pairs(item.items) do -- I sure hope this doesn't break anything! another.contained_in = item table.insert(all_items, another) end end end return all_items end function d.list(t, field) t = t or _G if type(t) ~= 'table' then d.printf("list needs a table.") return end local kt = {} for k, v in pairs(t) do table.insert(kt, k) end local sort_it = function() table.sort(kt) end pcall(sort_it) local out = {} for _, k in ipairs(kt) do local v = t[k] local listing = nil if type(v) == 'table' then if field then listing = v[field] else listing = (v.uiName or v.name or v.description) or listing end if listing then d.printf("%s: [%s] %s", d.to_s(k), d.to_s(v), d.to_s(listing)) end end end end function d.showlight() local p = d.partymembers() local total_value = 0 for _, char in ipairs(p) do local fuel = 0 local contained = 0 for _, item in pairs(d.all_items(char)) do if item.torch and item.fuel and item.fuel > 0 then fuel = fuel + item.fuel if item.contained_in then contained = contained + 1 end end end if fuel > 0 then total_value = total_value + fuel local xtra = '' if contained > 0 then xtra = d.sprintf(' (%d item%s in containers)', contained, contained == 1 and '' or 's') end printf(" %s: %.1f total fuel%s", char.name, total_value, xtra) end end if total_value > 0 then d.printf("Total fuel value: %d", total_value) else d.printf("Found no usable torches.") end end function d.showfood() local p = d.partymembers() local total_value = 0 local found = {} for _, char in ipairs(p) do for _, item in pairs(d.all_items(char)) do if item.consumable and item.nutritionValue and item.nutritionValue > 0 then if found[item.name] then found[item.name].count = found[item.name].count + 1 found[item.name].chars[char.name] = true else found[item.name] = { nutritionValue = item.nutritionValue, name = item.uiName or d.capitalize(item.name), count = 1, chars = { [char.name] = true }, contained = 0 } end if item.contained_in then found[item.name].contained = found[item.name].contained + 1 end end end end for name, details in pairs(found) do local xtra = '' if details.contained > 0 then xtra = d.sprintf(' (%d item%s in containers)', details.contained, details.contained == 1 and '' or 's') end local mult = '' if details.count > 1 then mult = d.sprintf(' x%d', details.count) end local who = d.keys(details.chars) d.printf(" %s%s (%d each) [%s]%s", details.name, mult, details.nutritionValue, table.concat(who, ", "), xtra) total_value = total_value + (details.nutritionValue * details.count) end if total_value > 0 then d.printf("Total food value: %d", total_value) else d.printf("Found no comestibles.") end end function d.fulldump(t) t = t or _G local visited = {} local dumpfile = io.open('g.txt', 'w') if dumpfile then d.full_dump_file(t, dumpfile, '', visited) end dumpfile:close() end function d.full_dump_file(t, dumpfile, prefix, visited) if type(t) ~= 'table' then d.fprintf(dumpfile, "%s%s\n", prefix, tostring(t)) return end if visited[t] then d.fprintf(dumpfile, "%s[already visited: %s]\n", prefix, d.to_s(t)) return end visited[t] = true local sorted = {} local keytrans = {} for k, _ in pairs(t) do local k2 = d.to_s(k) keytrans[k2] = k table.insert(sorted, k2) end table.sort(sorted) for _, k2 in ipairs(sorted) do local k = keytrans[k2] local v = t[k] if type(v) == 'table' then if visited[v] then d.fprintf(dumpfile, "%s%s = [%s] { already visited }\n", prefix, k, d.to_s(v)) elseif string.sub(k2, 1, 2) == '__' then d.fprintf(dumpfile, "%s%s = [%s] { skipping __ member }\n", prefix, k, d.to_s(v)) else d.fprintf(dumpfile, "%s%s = [%s] {\n", prefix, k, d.to_s(v)) d.full_dump_file(v, dumpfile, prefix .. ' ', visited) d.fprintf(dumpfile, "%s},\n", prefix) end else local q = '' if type(v) == 'string' then q = '"' end d.fprintf(dumpfile, "%s%s = %s%s%s,\n", prefix, k, q, d.to_s(v), q) end end end function d.iterate(t, maxdepth, callback, prefix, visited) if not t or type(t) ~= 'table' then d.printf("That's a %s, not a table.", type(t)) return end if maxdepth and maxdepth <= 0 then return end if not callback or type(callback) ~= 'function' then callback = d.print_item end prefix = prefix or '' visited = visited or {} visited[t] = true local count = 0 for k, v in pairs(t) do local k2 = d.to_s(k) count = count + 1 callback(prefix, k2, d.to_s(v), t, k, v) if type(v) == 'table' and not visited[v] and (string.sub(k2, 1, 2) ~= '__') then d.iterate(v, maxdepth and (maxdepth - 1), callback, prefix .. d.to_s(k) .. '.', visited) end end return count end function d.speed(multiplier) multiplier = multiplier or 1 if type(multiplier) ~= 'number' or multiplier < 0.01 or multiplier > 10 then d.printf("Stick to speeds in the .1-10 range.") return end gameMode.timeMultiplier = multiplier end function d.dump(t, depth) local count = d.iterate(t, depth, print_item, '', {}) end function d.globalize() d.printf("Polluting global namespace...") for k, v in pairs(d) do d.printf(" %s", k) _G[k] = v end end function d.hotkeys() config.developer = not config.developer config:apply() if config.developer then print("Developer hotkeys:") print(" h heal/cure everyone") print(" l give everyone a level") print(" m spawn a spider") print(" L go down one level, granting XP and items (can crash game)") print(" F1 toggle collisions") print(" F2 toggle diffuse mapping") print(" F3 toggle normal mapping") print(" F4 toggle ambient occlusion") print(" F5 debugging render mode cycle") print(" F6 wireframe") print(" F7 AI debug") print(" F8 sound debug") print(" F11 show more detailed map") else print("Developer mode off.") end end function d.intercept_die(...) d.last_die_event = { ... } local f = d.last_die_event[1].flags d.last_die_event[1].flags = math.floor(f / 16) * 16 + 15 return d.saveMonsterDie(...) end function d.share_exp() if Monster.die ~= d.intercept_die then d.saveMonsterDie = Monster.die Monster.die = d.intercept_die end end printf("Debug tools loaded. Developer mode %s.", config.developer and 'enabled' or 'disabled')