mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-16 11:42:45 +08:00
parent
221cc552c7
commit
f0212d54cf
75
docs/diy/08-debugging.rst
Normal file
75
docs/diy/08-debugging.rst
Normal file
|
@ -0,0 +1,75 @@
|
|||
.. SPDX-License-Identifier: GFDL-1.3-or-later
|
||||
|
||||
技巧:调试您的代码
|
||||
==================
|
||||
|
||||
Lua是一门很不错的语言。然而,其在调试上却稍有困难。网上或许能找到一些针对独立运行的Lua脚本的调试器(例如vscode能下载的到的各种Lua debugger),但却不适用于FK。
|
||||
|
||||
因此,本文将试图为你扫清Lua调试方面的障碍。
|
||||
|
||||
假设你正使用Windows系统,那么启动FK的时候,应该是一个游戏窗口+一个黑色的命令行窗口。调试工作基本就是在黑色窗口进行的。
|
||||
|
||||
使用debugger.lua
|
||||
----------------
|
||||
|
||||
这应该是最为推荐的一种做法了,FreeKill在lib中引用了debugger.lua作为调试库。
|
||||
|
||||
以下只简要介绍一下用法,最详细的详情请去项目官网查看: https://github.com/slembcke/debugger.lua
|
||||
|
||||
当你想要在代码中下断点时,就调用 ``dbg()`` 函数。当执行到这里时,就会停下来并在命令行中显示类似gdb的界面。
|
||||
|
||||
例如:
|
||||
|
||||
.. code:: lua
|
||||
|
||||
local room = player.room
|
||||
|
||||
dbg() -- 相当于下了断点,后面就可以来此进行调试
|
||||
player:drawCards(1)
|
||||
|
||||
上面的代码中就调用了debugger.lua,让程序进行了中断,然后命令行就进入了调试界面。
|
||||
|
||||
.. hint::
|
||||
|
||||
在默认的双击启用exe带有的命令行中,颜色可能会显示的非常奇怪。
|
||||
|
||||
如果你遇到了颜色不能正常显示的问题,推荐你使用Git Bash或者Windows Terminal之类的终端模拟器,然后在命令行中通过FreeKill.exe来启动游戏。
|
||||
|
||||
下面来说说调试的基本用法:使用 ``h`` 命令显示帮助信息。debugger.lua已经被我中文化了。
|
||||
|
||||
.. tip::
|
||||
|
||||
其实也可以用lua自带的 ``debug.debug()`` 进行交互式调试,不过功能比debugger.lua弱得多了。
|
||||
|
||||
.. warning::
|
||||
|
||||
在Linux上使用FreeKill -s开服时不能用这个调试器!因为stdin已经被服务端shell占用了,所以无法调试。
|
||||
|
||||
一些在调试中可能有用的函数
|
||||
--------------------------
|
||||
|
||||
在调试器中直接输入Lua语句就能执行。以下是一些可能用得到的函数:
|
||||
|
||||
print
|
||||
~~~~~
|
||||
|
||||
遇事不决print,这是当时没有调试器可用时候的措施。简单但却实用。
|
||||
|
||||
现在可以直接用debugger.lua的 p 命令输出表达式的值了,无需再自己写一堆。
|
||||
|
||||
p
|
||||
~~~
|
||||
|
||||
``p`` 也是个函数,是inspect库的包装。它能详细输出表中的所有值,包括元表。
|
||||
|
||||
因此在使用它输出和类相关的东西的时候还是放弃为好...
|
||||
|
||||
json.encode
|
||||
~~~~~~~~~~~
|
||||
|
||||
将不含循环引用的表转换为json字符串。或许会很有用吧。但是不如p就是了。
|
||||
|
||||
GameLogic:dumpEventStack()
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
输出当前的事件栈。在处理插结的时候能派上用场。
|
10
docs/diy/09-exppattern.rst
Normal file
10
docs/diy/09-exppattern.rst
Normal file
|
@ -0,0 +1,10 @@
|
|||
.. SPDX-License-Identifier: GFDL-1.3-or-later
|
||||
|
||||
解析:Exppattern
|
||||
================
|
||||
|
||||
所谓Exppattern,类似于各大编程语言中的正则表达式。不过和正则表达式不同的是,正则匹配的是字符串,而Exppattern匹配的对象是一张张卡牌。通过Exppattern可以判断各种卡牌的情况,比如这张牌是否符合“既是红桃,也是点数3-5的牌”等等一系列复杂的规则。
|
||||
|
||||
你可能也已经注意到了,在不少Room中的askFor...函数中,出现了很多次pattern参数。这个pattern就是Exppattern,它用来辅助确定询问的卡牌必须满足哪些需求。
|
||||
|
||||
|
|
@ -12,4 +12,6 @@ Diy文档
|
|||
04-newskill.rst
|
||||
05-trigger.rst
|
||||
06-active.rst
|
||||
03-events.rst
|
||||
07-events.rst
|
||||
08-debugging.rst
|
||||
09-exppattern.rst
|
||||
|
|
|
@ -121,7 +121,7 @@ function Card:initialize(name, suit, number, color)
|
|||
end
|
||||
|
||||
function Card:__tostring()
|
||||
return string.format("%s[%s %d]", self.name, self:getSuitString(), self.number)
|
||||
return string.format("<Card %s[%s %d]>", self.name, self:getSuitString(), self.number)
|
||||
end
|
||||
|
||||
--- 克隆特定卡牌并赋予花色与点数。
|
||||
|
|
|
@ -2,21 +2,7 @@
|
|||
|
||||
---@diagnostic disable: lowercase-global
|
||||
inspect = require "inspect"
|
||||
|
||||
DebugMode = true
|
||||
function PrintWhenMethodCall()
|
||||
local info = debug.getinfo(2)
|
||||
local name = info.name
|
||||
local line = info.currentline
|
||||
local namewhat = info.namewhat
|
||||
local shortsrc = info.short_src
|
||||
if (namewhat == "method") and
|
||||
(shortsrc ~= "[C]") and
|
||||
(not string.find(shortsrc, "/lib")) then
|
||||
print(shortsrc .. ":" .. line .. ": " .. name)
|
||||
end
|
||||
end
|
||||
--debug.sethook(PrintWhenMethodCall, "c")
|
||||
dbg = require "debugger"
|
||||
|
||||
function p(v) print(inspect(v)) end
|
||||
function pt(t) for k,v in pairs(t)do print(k,v) end end
|
||||
function pt(t) for k, v in pairs(t) do print(k, v) end end
|
||||
|
|
|
@ -16,14 +16,19 @@
|
|||
例如:
|
||||
slash,jink|2~4|spade;.|.|.|.|.|trick
|
||||
|
||||
你可以使用 '^' 符号表示否定,比如 ^heart 表示除了红桃之外的所有花色。
|
||||
否定型一样的可以与其他表达式并用,用 ',' 分割。
|
||||
如果要同时否定多项,则需要用括号: ^(heart, club) 等。
|
||||
注:这种括号不支持嵌套否定。
|
||||
|
||||
]]--
|
||||
|
||||
---@class Matcher
|
||||
---@field public name string[]
|
||||
---@field public trueName string[]
|
||||
---@field public number integer[]
|
||||
---@field public suit string[]
|
||||
---@field public place string[]
|
||||
---@field public generalName string[]
|
||||
---@field public name string[]
|
||||
---@field public cardType string[]
|
||||
---@field public id integer[]
|
||||
|
||||
|
@ -34,23 +39,44 @@ local numbertable = {
|
|||
["K"] = 13,
|
||||
}
|
||||
|
||||
local suittable = {
|
||||
[Card.Spade] = "spade",
|
||||
[Card.Club] = "club",
|
||||
[Card.Heart] = "heart",
|
||||
[Card.Diamond] = "diamond",
|
||||
}
|
||||
|
||||
local placetable = {
|
||||
[Card.PlayerHand] = "hand",
|
||||
[Card.PlayerEquip] = "equip",
|
||||
}
|
||||
|
||||
local typetable = {
|
||||
[Card.TypeBasic] = "basic",
|
||||
[Card.TypeTrick] = "trick",
|
||||
[Card.TypeEquip] = "equip",
|
||||
}
|
||||
local function matchSingleKey(matcher, card, key)
|
||||
local match = matcher[key]
|
||||
if not match then return true end
|
||||
local val = card[key]
|
||||
if key == "suit" then
|
||||
val = card:getSuitString()
|
||||
elseif key == "cardType" then
|
||||
val = card:getTypeString()
|
||||
elseif key == "place" then
|
||||
val = placetable[Fk:currentRoom():getCardArea(card.id)]
|
||||
if not val then
|
||||
for _, p in ipairs(Fk:currentRoom().alive_players) do
|
||||
val = p:getPileNameOfId(card.id)
|
||||
if val then break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if table.contains(match, val) then
|
||||
return true
|
||||
else
|
||||
local neg = match.neg
|
||||
if not neg then return false end
|
||||
for _, t in ipairs(neg) do
|
||||
if type(t) == "table" then
|
||||
if not table.contains(t, val) then return true end
|
||||
else
|
||||
if t ~= val then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param matcher Matcher
|
||||
---@param card Card
|
||||
|
@ -59,50 +85,13 @@ local function matchCard(matcher, card)
|
|||
card = Fk:getCardById(card)
|
||||
end
|
||||
|
||||
if matcher.name and not table.contains(matcher.name, card.name) and
|
||||
not table.contains(matcher.name, card.trueName) then
|
||||
return false
|
||||
end
|
||||
|
||||
if matcher.number and not table.contains(matcher.number, card.number) then
|
||||
return false
|
||||
end
|
||||
|
||||
if matcher.suit and not table.contains(matcher.suit, card:getSuitString()) then
|
||||
return false
|
||||
end
|
||||
|
||||
if matcher.place and not table.contains(
|
||||
matcher.place,
|
||||
placetable[Fk:currentRoom():getCardArea(card.id)]
|
||||
) then
|
||||
local piles = table.filter(matcher.place, function(e)
|
||||
return not table.contains(placetable, e)
|
||||
end)
|
||||
for _, pi in ipairs(piles) do
|
||||
if ClientInstance then
|
||||
if Self:getPileNameOfId(card.id) == pi then return true end
|
||||
else
|
||||
for _, p in ipairs(RoomInstance.alive_players) do
|
||||
local pile = p:getPileNameOfId(card.id)
|
||||
if pile == pi then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- TODO: generalName
|
||||
|
||||
if matcher.cardType and not table.contains(matcher.cardType, typetable[card.type]) then
|
||||
return false
|
||||
end
|
||||
|
||||
if matcher.id and not table.contains(matcher.id, card.id) then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
return matchSingleKey(matcher, card, "trueName")
|
||||
and matchSingleKey(matcher, card, "number")
|
||||
and matchSingleKey(matcher, card, "suit")
|
||||
and matchSingleKey(matcher, card, "place")
|
||||
and matchSingleKey(matcher, card, "name")
|
||||
and matchSingleKey(matcher, card, "cardType")
|
||||
and matchSingleKey(matcher, card, "id")
|
||||
end
|
||||
|
||||
local function hasIntersection(a, b)
|
||||
|
@ -119,6 +108,9 @@ local function hasIntersection(a, b)
|
|||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: 判断含有neg的两个matcher
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
|
@ -126,11 +118,11 @@ end
|
|||
---@param b Matcher
|
||||
local function matchMatcher(a, b)
|
||||
local keys = {
|
||||
"name",
|
||||
"trueName",
|
||||
"number",
|
||||
"suit",
|
||||
"place",
|
||||
"generalName",
|
||||
"name",
|
||||
"cardType",
|
||||
"id",
|
||||
}
|
||||
|
@ -144,6 +136,104 @@ local function matchMatcher(a, b)
|
|||
return true
|
||||
end
|
||||
|
||||
local function parseNegative(list)
|
||||
local bracket = nil
|
||||
local toRemove = {}
|
||||
for i, element in ipairs(list) do
|
||||
if element[1] == "^" or bracket then
|
||||
list.neg = list.neg or {}
|
||||
table.insert(toRemove, 1, i)
|
||||
if element[1] == "^" and element[2] == "(" then
|
||||
if bracket then
|
||||
error("pattern syntax error. Cannot use nested bracket.")
|
||||
else
|
||||
bracket = {}
|
||||
end
|
||||
element = element:sub(3)
|
||||
else
|
||||
if element[1] == "^" then
|
||||
element = element:sub(2)
|
||||
end
|
||||
end
|
||||
|
||||
local eofBracket
|
||||
if element:endsWith(")") then
|
||||
eofBracket = true
|
||||
element = element:sub(1, -2)
|
||||
end
|
||||
|
||||
if eofBracket then
|
||||
if not bracket then
|
||||
error('pattern syntax error. No matching bracket.')
|
||||
else
|
||||
table.insert(bracket, element)
|
||||
table.insert(list.neg, bracket)
|
||||
bracket = nil
|
||||
end
|
||||
else
|
||||
if bracket then
|
||||
table.insert(bracket, element)
|
||||
else
|
||||
table.insert(list.neg, element)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, i in ipairs(toRemove) do
|
||||
table.remove(list, i)
|
||||
end
|
||||
end
|
||||
|
||||
local function parseNumToTable(from, dest)
|
||||
for _, num in ipairs(from) do
|
||||
if type(num) ~= "string" then goto continue end
|
||||
local n = tonumber(num)
|
||||
if not n then
|
||||
n = numbertable[num]
|
||||
end
|
||||
if n then
|
||||
table.insertIfNeed(dest, n)
|
||||
else
|
||||
if string.find(num, "~") then
|
||||
local s, e = table.unpack(num:split("~"))
|
||||
local start = tonumber(s)
|
||||
if not start then
|
||||
start = numbertable[s]
|
||||
end
|
||||
local _end = tonumber(e)
|
||||
if not _end then
|
||||
_end = numbertable[e]
|
||||
end
|
||||
|
||||
for i = start, _end do
|
||||
table.insertIfNeed(dest, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
local function parseRawNumTable(tab)
|
||||
local ret = {}
|
||||
parseNumToTable(tab, ret)
|
||||
|
||||
if tab.neg then
|
||||
ret.neg = {}
|
||||
parseNumToTable(tab.neg, ret.neg)
|
||||
|
||||
for _, t in ipairs(tab.neg) do
|
||||
if type(t) == "table" then
|
||||
local tmp = {}
|
||||
parseNumToTable(t, tmp)
|
||||
table.insert(ret.neg, tmp)
|
||||
end
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function parseMatcher(str)
|
||||
local t = str:split("|")
|
||||
if #t < 7 then
|
||||
|
@ -156,56 +246,57 @@ local function parseMatcher(str)
|
|||
t[i] = item:split(",")
|
||||
end
|
||||
|
||||
for _, list in ipairs(t) do
|
||||
parseNegative(list)
|
||||
end
|
||||
|
||||
local ret = {} ---@type Matcher
|
||||
ret.name = not table.contains(t[1], ".") and t[1] or nil
|
||||
ret.trueName = not table.contains(t[1], ".") and t[1] or nil
|
||||
|
||||
if not table.contains(t[2], ".") then
|
||||
ret.number = {}
|
||||
for _, num in ipairs(t[2]) do
|
||||
local n = tonumber(num)
|
||||
if not n then
|
||||
n = numbertable[num]
|
||||
end
|
||||
if n then
|
||||
table.insertIfNeed(ret.number, n)
|
||||
else
|
||||
if string.find(num, "~") then
|
||||
local s, e = table.unpack(num:split("~"))
|
||||
local start = tonumber(s)
|
||||
if not start then
|
||||
start = numbertable[s]
|
||||
end
|
||||
local _end = tonumber(e)
|
||||
if not _end then
|
||||
_end = numbertable[e]
|
||||
end
|
||||
|
||||
for i = start, _end do
|
||||
table.insertIfNeed(ret.number, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ret.number = parseRawNumTable(t[2])
|
||||
end
|
||||
|
||||
ret.suit = not table.contains(t[3], ".") and t[3] or nil
|
||||
ret.place = not table.contains(t[4], ".") and t[4] or nil
|
||||
ret.generalName = not table.contains(t[5], ".") and t[5] or nil
|
||||
ret.name = not table.contains(t[5], ".") and t[5] or nil
|
||||
ret.cardType = not table.contains(t[6], ".") and t[6] or nil
|
||||
|
||||
if not table.contains(t[7], ".") then
|
||||
ret.id = {}
|
||||
for _, num in ipairs(t[6]) do
|
||||
local n = tonumber(num)
|
||||
if n and n > 0 then
|
||||
table.insertIfNeed(ret.id, n)
|
||||
end
|
||||
end
|
||||
ret.id = parseRawNumTable(t[7])
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local function matcherKeyToString(tab)
|
||||
if not tab then return "." end
|
||||
local ret = table.concat(tab, ",")
|
||||
if tab.neg then
|
||||
for _, t in ipairs(tab.neg) do
|
||||
if ret ~= "" then ret = ret .. "," end
|
||||
if type(t) == "table" then
|
||||
ret = ret .. ("^(" .. table.concat(t, ",") .. ")")
|
||||
else
|
||||
ret = ret .. "^" .. t
|
||||
end
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function matcherToString(matcher)
|
||||
return table.concat({
|
||||
matcherKeyToString(matcher.trueName),
|
||||
matcherKeyToString(matcher.number),
|
||||
matcherKeyToString(matcher.suit),
|
||||
matcherKeyToString(matcher.place),
|
||||
matcherKeyToString(matcher.name),
|
||||
matcherKeyToString(matcher.cardType),
|
||||
matcherKeyToString(matcher.id),
|
||||
}, "|")
|
||||
end
|
||||
|
||||
---@class Exppattern: Object
|
||||
---@field public matchers Matcher[]
|
||||
local Exppattern = class("Exppattern")
|
||||
|
@ -266,4 +357,13 @@ function Exppattern:matchExp(exp)
|
|||
return false
|
||||
end
|
||||
|
||||
function Exppattern:__tostring()
|
||||
local ret = ""
|
||||
for i, matcher in ipairs(self.matchers) do
|
||||
if i > 1 then ret = ret .. ";" end
|
||||
ret = ret .. matcherToString(matcher)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
return Exppattern
|
||||
|
|
|
@ -99,7 +99,7 @@ function Player:initialize()
|
|||
end
|
||||
|
||||
function Player:__tostring()
|
||||
return string.format("%s #%d", self.id < 0 and "Bot" or "Player", math.abs(self.id))
|
||||
return string.format("<%s %d>", self.id < 0 and "Bot" or "Player", math.abs(self.id))
|
||||
end
|
||||
|
||||
--- 设置角色、体力、技能。
|
||||
|
|
|
@ -53,6 +53,10 @@ function Skill:initialize(name, frequency)
|
|||
self.attached_equip = nil
|
||||
end
|
||||
|
||||
function Skill:__tostring()
|
||||
return "<Skill " .. self.name .. ">"
|
||||
end
|
||||
|
||||
--- 为一个技能增加相关技能。
|
||||
---@param skill Skill @ 技能
|
||||
function Skill:addRelatedSkill(skill)
|
||||
|
|
590
lua/lib/debugger.lua
Normal file
590
lua/lib/debugger.lua
Normal file
|
@ -0,0 +1,590 @@
|
|||
--[[
|
||||
Copyright (c) 2023 Scott Lembcke and Howling Moon Software
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
TODO:
|
||||
* Print short function arguments as part of stack location.
|
||||
* Properly handle being reentrant due to coroutines.
|
||||
]]
|
||||
|
||||
-- notify 汉化 并根据fk/lua 5.4实际情况魔改
|
||||
|
||||
local dbg
|
||||
|
||||
-- ** FreeKill **: local the deleted global var here
|
||||
local io = io
|
||||
local os = os
|
||||
local load = load
|
||||
|
||||
-- Use ANSI color codes in the prompt by default.
|
||||
local COLOR_GRAY = ""
|
||||
local COLOR_RED = ""
|
||||
local COLOR_BLUE = ""
|
||||
local COLOR_YELLOW = ""
|
||||
local COLOR_RESET = ""
|
||||
local GREEN_CARET = " => "
|
||||
|
||||
local function pretty(obj, max_depth)
|
||||
if max_depth == nil then max_depth = dbg.pretty_depth end
|
||||
|
||||
-- Returns true if a table has a __tostring metamethod.
|
||||
local function coerceable(tbl)
|
||||
local meta = getmetatable(tbl)
|
||||
return (meta and meta.__tostring)
|
||||
end
|
||||
|
||||
local function recurse(obj, depth)
|
||||
if type(obj) == "string" then
|
||||
-- Dump the string so that escape sequences are printed.
|
||||
return string.format("%q", obj)
|
||||
elseif type(obj) == "table" and depth < max_depth and not coerceable(obj) then
|
||||
local str = "{"
|
||||
|
||||
for k, v in pairs(obj) do
|
||||
local pair = pretty(k, 0).." = "..recurse(v, depth + 1)
|
||||
str = str..(str == "{" and pair or ", "..pair)
|
||||
end
|
||||
|
||||
return str.."}"
|
||||
else
|
||||
-- tostring() can fail if there is an error in a __tostring metamethod.
|
||||
local success, value = pcall(function() return tostring(obj) end)
|
||||
return (success and value or "<!!__tostring 元方法出错!!>")
|
||||
end
|
||||
end
|
||||
|
||||
return recurse(obj, 0)
|
||||
end
|
||||
|
||||
-- The stack level that cmd_* functions use to access locals or info
|
||||
-- The structure of the code very carefully ensures this.
|
||||
local CMD_STACK_LEVEL = 6
|
||||
|
||||
-- Location of the top of the stack outside of the debugger.
|
||||
-- Adjusted by some debugger entrypoints.
|
||||
local stack_top = 0
|
||||
|
||||
-- The current stack frame index.
|
||||
-- Changed using the up/down commands
|
||||
local stack_inspect_offset = 0
|
||||
|
||||
-- LuaJIT has an off by one bug when setting local variables.
|
||||
local LUA_JIT_SETLOCAL_WORKAROUND = 0
|
||||
|
||||
-- Default dbg.read function
|
||||
local function dbg_read(prompt)
|
||||
dbg.write(prompt)
|
||||
io.flush()
|
||||
return io.read()
|
||||
end
|
||||
|
||||
-- Default dbg.write function
|
||||
local function dbg_write(str)
|
||||
io.write(str)
|
||||
end
|
||||
|
||||
local function dbg_writeln(str, ...)
|
||||
if select("#", ...) == 0 then
|
||||
dbg.write((str or "<NULL>").."\n")
|
||||
else
|
||||
dbg.write(string.format(str.."\n", ...))
|
||||
end
|
||||
end
|
||||
|
||||
local function format_loc(file, line) return COLOR_BLUE..file..COLOR_RESET..":"..COLOR_YELLOW..line..COLOR_RESET end
|
||||
local function format_stack_frame_info(info)
|
||||
local filename = info.source:match("@(.*)")
|
||||
local source = filename and dbg.shorten_path(filename) or info.short_src
|
||||
local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat)
|
||||
local name = (info.name and "'"..COLOR_BLUE..info.name..COLOR_RESET.."'" or format_loc(source, info.linedefined))
|
||||
return format_loc(source, info.currentline)..", 在"..namewhat.." "..name
|
||||
end
|
||||
|
||||
local repl
|
||||
|
||||
-- Return false for stack frames without source,
|
||||
-- which includes C frames, Lua bytecode, and `loadstring` functions
|
||||
local function frame_has_line(info) return info.currentline >= 0 end
|
||||
|
||||
local function hook_factory(repl_threshold)
|
||||
return function(offset, reason)
|
||||
return function(event, _)
|
||||
-- Skip events that don't have line information.
|
||||
if not frame_has_line(debug.getinfo(2)) then return end
|
||||
|
||||
-- Tail calls are specifically ignored since they also will have tail returns to balance out.
|
||||
if event == "call" then
|
||||
offset = offset + 1
|
||||
elseif event == "return" and offset > repl_threshold then
|
||||
offset = offset - 1
|
||||
elseif event == "line" and offset <= repl_threshold then
|
||||
repl(reason)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local hook_step = hook_factory(1)
|
||||
local hook_next = hook_factory(0)
|
||||
local hook_finish = hook_factory(-1)
|
||||
|
||||
-- Create a table of all the locally accessible variables.
|
||||
-- Globals are not included when running the locals command, but are when running the print command.
|
||||
local function local_bindings(offset, include_globals)
|
||||
local level = offset + stack_inspect_offset + CMD_STACK_LEVEL
|
||||
local func = debug.getinfo(level).func
|
||||
local bindings = {}
|
||||
|
||||
-- Retrieve the upvalues
|
||||
do local i = 1; while true do
|
||||
local name, value = debug.getupvalue(func, i)
|
||||
if not name then break end
|
||||
bindings[name] = value
|
||||
i = i + 1
|
||||
end end
|
||||
|
||||
-- Retrieve the locals (overwriting any upvalues)
|
||||
do local i = 1; while true do
|
||||
local name, value = debug.getlocal(level, i)
|
||||
if not name then break end
|
||||
bindings[name] = value
|
||||
i = i + 1
|
||||
end end
|
||||
|
||||
-- Retrieve the varargs (works in Lua 5.2 and LuaJIT)
|
||||
local varargs = {}
|
||||
do local i = 1; while true do
|
||||
local name, value = debug.getlocal(level, -i)
|
||||
if not name then break end
|
||||
varargs[i] = value
|
||||
i = i + 1
|
||||
end end
|
||||
if #varargs > 0 then bindings["..."] = varargs end
|
||||
|
||||
if include_globals then
|
||||
-- In Lua 5.2, you have to get the environment table from the function's locals.
|
||||
local env = (_VERSION <= "Lua 5.1" and getfenv(func) or bindings._ENV)
|
||||
return setmetatable(bindings, {__index = env or _G})
|
||||
else
|
||||
return bindings
|
||||
end
|
||||
end
|
||||
|
||||
-- Used as a __newindex metamethod to modify variables in cmd_eval().
|
||||
local function mutate_bindings(_, name, value)
|
||||
local FUNC_STACK_OFFSET = 3 -- Stack depth of this function.
|
||||
local level = stack_inspect_offset + FUNC_STACK_OFFSET + CMD_STACK_LEVEL
|
||||
|
||||
-- Set a local.
|
||||
do local i = 1; repeat
|
||||
local var = debug.getlocal(level, i)
|
||||
if name == var then
|
||||
dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."设置了局部变量 "..COLOR_BLUE..name..COLOR_RESET)
|
||||
return debug.setlocal(level + LUA_JIT_SETLOCAL_WORKAROUND, i, value)
|
||||
end
|
||||
i = i + 1
|
||||
until var == nil end
|
||||
|
||||
-- Set an upvalue.
|
||||
local func = debug.getinfo(level).func
|
||||
do local i = 1; repeat
|
||||
local var = debug.getupvalue(func, i)
|
||||
if name == var then
|
||||
dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."设置了上值 "..COLOR_BLUE..name..COLOR_RESET)
|
||||
return debug.setupvalue(func, i, value)
|
||||
end
|
||||
i = i + 1
|
||||
until var == nil end
|
||||
|
||||
-- Set a global.
|
||||
dbg_writeln(COLOR_YELLOW.."debugger.lua"..GREEN_CARET.."设置了全局变量 "..COLOR_BLUE..name..COLOR_RESET)
|
||||
_G[name] = value
|
||||
end
|
||||
|
||||
-- Compile an expression with the given variable bindings.
|
||||
local function compile_chunk(block, env)
|
||||
local source = "debugger.lua REPL"
|
||||
local chunk = nil
|
||||
|
||||
if _VERSION <= "Lua 5.1" then
|
||||
chunk = loadstring(block, source)
|
||||
if chunk then setfenv(chunk, env) end
|
||||
else
|
||||
-- The Lua 5.2 way is a bit cleaner
|
||||
chunk = load(block, source, "t", env)
|
||||
end
|
||||
|
||||
if not chunk then dbg_writeln(COLOR_RED.."错误: 无法编译代码:\n"..COLOR_RESET..block) end
|
||||
return chunk
|
||||
end
|
||||
|
||||
local SOURCE_CACHE = {}
|
||||
|
||||
local function where(info, context_lines)
|
||||
local source = SOURCE_CACHE[info.source]
|
||||
if not source then
|
||||
source = {}
|
||||
local filename = info.source:match("@(.*)")
|
||||
if filename then
|
||||
pcall(function() for line in io.lines(filename) do table.insert(source, line) end end)
|
||||
elseif info.source then
|
||||
for line in info.source:gmatch("(.-)\n") do table.insert(source, line) end
|
||||
end
|
||||
SOURCE_CACHE[info.source] = source
|
||||
end
|
||||
|
||||
if source and source[info.currentline] then
|
||||
for i = info.currentline - context_lines, info.currentline + context_lines do
|
||||
local tab_or_caret = (i == info.currentline and GREEN_CARET or " ")
|
||||
local line = source[i]
|
||||
if line then dbg_writeln(COLOR_GRAY.."% 4d"..tab_or_caret.."%s", i, line) end
|
||||
end
|
||||
else
|
||||
dbg_writeln(COLOR_RED.."错误: 源码不可用: "..COLOR_BLUE..info.short_src);
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Wee version differences
|
||||
local unpack = unpack or table.unpack
|
||||
local pack = function(...) return {n = select("#", ...), ...} end
|
||||
|
||||
local function cmd_step()
|
||||
stack_inspect_offset = stack_top
|
||||
return true, hook_step
|
||||
end
|
||||
|
||||
local function cmd_next()
|
||||
stack_inspect_offset = stack_top
|
||||
return true, hook_next
|
||||
end
|
||||
|
||||
local function cmd_finish()
|
||||
local offset = stack_top - stack_inspect_offset
|
||||
stack_inspect_offset = stack_top
|
||||
return true, offset < 0 and hook_factory(offset - 1) or hook_finish
|
||||
end
|
||||
|
||||
local function cmd_print(expr)
|
||||
local env = local_bindings(1, true)
|
||||
local chunk = compile_chunk("return "..expr, env)
|
||||
if chunk == nil then return false end
|
||||
|
||||
-- Call the chunk and collect the results.
|
||||
local results = pack(pcall(chunk, unpack(rawget(env, "...") or {})))
|
||||
|
||||
-- The first result is the pcall error.
|
||||
if not results[1] then
|
||||
dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET.." "..results[2])
|
||||
else
|
||||
local output = ""
|
||||
for i = 2, results.n do
|
||||
output = output..(i ~= 2 and ", " or "")..dbg.pretty(results[i])
|
||||
end
|
||||
|
||||
if output == "" then output = "<无返回值>" end
|
||||
dbg_writeln(COLOR_BLUE..expr.. GREEN_CARET..output)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function cmd_eval(code)
|
||||
local env = local_bindings(1, true)
|
||||
local mutable_env = setmetatable({}, {
|
||||
__index = env,
|
||||
__newindex = mutate_bindings,
|
||||
})
|
||||
|
||||
local chunk = compile_chunk(code, mutable_env)
|
||||
if chunk == nil then return false end
|
||||
|
||||
-- Call the chunk and collect the results.
|
||||
local success, err = pcall(chunk, unpack(rawget(env, "...") or {}))
|
||||
if not success then
|
||||
dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET.." "..tostring(err))
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function cmd_down()
|
||||
local offset = stack_inspect_offset
|
||||
local info
|
||||
|
||||
repeat -- Find the next frame with a file.
|
||||
offset = offset + 1
|
||||
info = debug.getinfo(offset + CMD_STACK_LEVEL)
|
||||
until not info or frame_has_line(info)
|
||||
|
||||
if info then
|
||||
stack_inspect_offset = offset
|
||||
dbg_writeln("目前所在的栈帧: "..format_stack_frame_info(info))
|
||||
if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end
|
||||
else
|
||||
info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)
|
||||
dbg_writeln("已经位于栈底。")
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function cmd_up()
|
||||
local offset = stack_inspect_offset
|
||||
local info
|
||||
|
||||
repeat -- Find the next frame with a file.
|
||||
offset = offset - 1
|
||||
if offset < stack_top then info = nil; break end
|
||||
info = debug.getinfo(offset + CMD_STACK_LEVEL)
|
||||
until frame_has_line(info)
|
||||
|
||||
if info then
|
||||
stack_inspect_offset = offset
|
||||
dbg_writeln("目前所在的栈帧: "..format_stack_frame_info(info))
|
||||
if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end
|
||||
else
|
||||
info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)
|
||||
dbg_writeln("已经位于栈顶。")
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function cmd_where(context_lines)
|
||||
local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL)
|
||||
return (info and where(info, tonumber(context_lines) or 5))
|
||||
end
|
||||
|
||||
local function cmd_trace()
|
||||
dbg_writeln("目前在栈帧 %d", stack_inspect_offset - stack_top)
|
||||
local i = 0; while true do
|
||||
local info = debug.getinfo(stack_top + CMD_STACK_LEVEL + i)
|
||||
if not info then break end
|
||||
|
||||
local is_current_frame = (i + stack_top == stack_inspect_offset)
|
||||
local tab_or_caret = (is_current_frame and GREEN_CARET or " ")
|
||||
dbg_writeln(COLOR_GRAY.."% 4d"..COLOR_RESET..tab_or_caret.."%s", i, format_stack_frame_info(info))
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function cmd_locals()
|
||||
local bindings = local_bindings(1, false)
|
||||
|
||||
-- Get all the variable binding names and sort them
|
||||
local keys = {}
|
||||
for k, _ in pairs(bindings) do table.insert(keys, k) end
|
||||
table.sort(keys)
|
||||
|
||||
for _, k in ipairs(keys) do
|
||||
local v = bindings[k]
|
||||
|
||||
-- Skip the debugger object itself, "(*internal)" values, and Lua 5.2's _ENV object.
|
||||
if not rawequal(v, dbg) and k ~= "_ENV" and not k:match("%(.*%)") then
|
||||
dbg_writeln(" "..COLOR_BLUE..k.. GREEN_CARET..dbg.pretty(v))
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function cmd_help()
|
||||
dbg.write(""
|
||||
..COLOR_BLUE.." <回车>"..GREEN_CARET.."重复执行上一条命令\n"
|
||||
..COLOR_BLUE.." c"..COLOR_YELLOW.."(ontinue)"..GREEN_CARET.."继续执行代码\n"
|
||||
..COLOR_BLUE.." s"..COLOR_YELLOW.."(tep)"..GREEN_CARET.."单步执行下一行 (会深入到函数中)\n"
|
||||
..COLOR_BLUE.." n"..COLOR_YELLOW.."(ext)"..GREEN_CARET.."单步执行下一行 (不深入到函数调用)\n"
|
||||
..COLOR_BLUE.." f"..COLOR_YELLOW.."(inish)"..GREEN_CARET.."一直执行直到此函数返回\n"
|
||||
..COLOR_BLUE.." u"..COLOR_YELLOW.."(p)"..GREEN_CARET.."上移一个栈帧\n"
|
||||
..COLOR_BLUE.." d"..COLOR_YELLOW.."(own)"..GREEN_CARET.."下移一个栈帧\n"
|
||||
..COLOR_BLUE.." w"..COLOR_YELLOW.."(here) "..COLOR_BLUE.."[行数]"..GREEN_CARET.."打印出当前行周围的代码\n"
|
||||
..COLOR_BLUE.." e"..COLOR_YELLOW.."(val) "..COLOR_BLUE.."[语句]"..GREEN_CARET.."执行一个语句\n"
|
||||
..COLOR_BLUE.." p"..COLOR_YELLOW.."(rint) "..COLOR_BLUE.."[表达式]"..GREEN_CARET.."求出表达式的值,并打印出结果\n"
|
||||
..COLOR_BLUE.." t"..COLOR_YELLOW.."(race)"..GREEN_CARET.."打印函数调用栈\n"
|
||||
..COLOR_BLUE.." l"..COLOR_YELLOW.."(ocals)"..GREEN_CARET.."打印函数参数、局部变量和上值\n"
|
||||
..COLOR_BLUE.." h"..COLOR_YELLOW.."(elp)"..GREEN_CARET.."打印这条消息\n"
|
||||
-- ..COLOR_BLUE.." q"..COLOR_YELLOW.."(uit)"..GREEN_CARET.."结束调试,继续执行代码\n"
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
local last_cmd = false
|
||||
|
||||
local commands = {
|
||||
["^c$"] = function() return true end,
|
||||
["^s$"] = cmd_step,
|
||||
["^n$"] = cmd_next,
|
||||
["^f$"] = cmd_finish,
|
||||
["^p%s+(.*)$"] = cmd_print,
|
||||
["^e%s+(.*)$"] = cmd_eval,
|
||||
["^u$"] = cmd_up,
|
||||
["^d$"] = cmd_down,
|
||||
["^w%s*(%d*)$"] = cmd_where,
|
||||
["^t$"] = cmd_trace,
|
||||
["^l$"] = cmd_locals,
|
||||
["^h$"] = cmd_help,
|
||||
["^q$"] = function() dbg.exit(0); return true end,
|
||||
}
|
||||
|
||||
local function match_command(line)
|
||||
for pat, func in pairs(commands) do
|
||||
-- Return the matching command and capture argument.
|
||||
if line:find(pat) then return func, line:match(pat) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Run a command line
|
||||
-- Returns true if the REPL should exit and the hook function factory
|
||||
local function run_command(line)
|
||||
-- GDB/LLDB exit on ctrl-d
|
||||
if line == nil then dbg.exit(1); return true end
|
||||
|
||||
-- Re-execute the last command if you press return.
|
||||
if line == "" then line = last_cmd or "h" end
|
||||
|
||||
local command, command_arg = match_command(line)
|
||||
if command then
|
||||
last_cmd = line
|
||||
-- unpack({...}) prevents tail call elimination so the stack frame indices are predictable.
|
||||
return unpack({command(command_arg)})
|
||||
elseif dbg.auto_eval then
|
||||
return unpack({cmd_eval(line)})
|
||||
else
|
||||
dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET.." 无法识别命令 '%s'。\n输入 'h' 并按下回车键来查看命令列表。", line)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
repl = function(reason)
|
||||
-- Skip frames without source info.
|
||||
while not frame_has_line(debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)) do
|
||||
stack_inspect_offset = stack_inspect_offset + 1
|
||||
end
|
||||
|
||||
local info = debug.getinfo(stack_inspect_offset + CMD_STACK_LEVEL - 3)
|
||||
reason = reason and (COLOR_YELLOW.."由于 "..COLOR_RED..reason..GREEN_CARET.." 中断执行\n") or ""
|
||||
dbg_writeln(reason..format_stack_frame_info(info))
|
||||
|
||||
if tonumber(dbg.auto_where) then where(info, dbg.auto_where) end
|
||||
|
||||
repeat
|
||||
local success, done, hook = pcall(run_command, dbg.read(COLOR_RED.."(dbg) "..COLOR_RESET))
|
||||
if success then
|
||||
debug.sethook(hook and hook(0), "crl")
|
||||
else
|
||||
local message = COLOR_RED.."INTERNAL DEBUGGER.LUA ERROR. ABORTING\n:"..COLOR_RESET.." "..done
|
||||
dbg_writeln(message)
|
||||
error(message)
|
||||
end
|
||||
until done
|
||||
end
|
||||
|
||||
-- Make the debugger object callable like a function.
|
||||
dbg = setmetatable({}, {
|
||||
__call = function(_, condition, top_offset, source)
|
||||
if condition then return end
|
||||
|
||||
top_offset = (top_offset or 0)
|
||||
stack_inspect_offset = top_offset
|
||||
stack_top = top_offset
|
||||
|
||||
debug.sethook(hook_next(1, source or "dbg()"), "crl")
|
||||
return
|
||||
end,
|
||||
})
|
||||
|
||||
-- Expose the debugger's IO functions.
|
||||
dbg.read = dbg_read
|
||||
dbg.write = dbg_write
|
||||
dbg.shorten_path = function (path) return path end
|
||||
dbg.exit = function() end
|
||||
|
||||
dbg.writeln = dbg_writeln
|
||||
|
||||
dbg.pretty_depth = 3
|
||||
dbg.pretty = pretty
|
||||
dbg.pp = function(value, depth) dbg_writeln(dbg.pretty(value, depth)) end
|
||||
|
||||
dbg.auto_where = 1
|
||||
dbg.auto_eval = true
|
||||
|
||||
local lua_error, lua_assert = error, assert
|
||||
|
||||
-- Works like error(), but invokes the debugger.
|
||||
function dbg.error(err, level)
|
||||
level = level or 1
|
||||
dbg_writeln(COLOR_RED.."错误: "..COLOR_RESET..dbg.pretty(err))
|
||||
dbg(false, level, "dbg.error()")
|
||||
|
||||
lua_error(err, level)
|
||||
end
|
||||
|
||||
-- Works like assert(), but invokes the debugger on a failure.
|
||||
function dbg.assert(condition, message)
|
||||
if not condition then
|
||||
dbg_writeln(COLOR_RED.."错误:"..COLOR_RESET..message)
|
||||
dbg(false, 1, "dbg.assert()")
|
||||
end
|
||||
|
||||
return lua_assert(condition, message)
|
||||
end
|
||||
|
||||
-- Works like pcall(), but invokes the debugger on an error.
|
||||
function dbg.call(f, ...)
|
||||
return xpcall(f, function(err)
|
||||
dbg_writeln(COLOR_RED.."错误: "..COLOR_RESET..dbg.pretty(err))
|
||||
dbg(false, 1, "dbg.call()")
|
||||
|
||||
return err
|
||||
end, ...)
|
||||
end
|
||||
|
||||
-- Error message handler that can be used with lua_pcall().
|
||||
function dbg.msgh(...)
|
||||
if debug.getinfo(2) then
|
||||
dbg_writeln(COLOR_RED.."错误: "..COLOR_RESET..dbg.pretty(...))
|
||||
dbg(false, 1, "dbg.msgh()")
|
||||
else
|
||||
dbg_writeln(COLOR_RED.."debugger.lua: "..COLOR_RESET.."Lua代码中未发生错误。将在 dbg_pcall() 完成后继续执行代码。")
|
||||
end
|
||||
|
||||
return ...
|
||||
end
|
||||
|
||||
-- Assume stdin/out are TTYs unless we can use LuaJIT's FFI to properly check them.
|
||||
local stdin_isatty = true
|
||||
local stdout_isatty = true
|
||||
|
||||
-- Conditionally enable color support.
|
||||
local color_maybe_supported = (stdout_isatty and os.getenv("TERM") and os.getenv("TERM") ~= "dumb")
|
||||
if color_maybe_supported and not os.getenv("DBG_NOCOLOR") then
|
||||
COLOR_GRAY = string.char(27) .. "[90m"
|
||||
COLOR_RED = string.char(27) .. "[91m"
|
||||
COLOR_BLUE = string.char(27) .. "[94m"
|
||||
COLOR_YELLOW = string.char(27) .. "[33m"
|
||||
COLOR_RESET = string.char(27) .. "[0m"
|
||||
GREEN_CARET = string.char(27) .. "[92m => "..COLOR_RESET
|
||||
end
|
||||
|
||||
return dbg
|
Loading…
Reference in New Issue
Block a user