/ld26

To get this branch, use:
bzr branch /bzr/ld26
1 by Josh C
zoetrope 1.4
1
-- Class: DebugConsole
2
-- It can be used to keep track of fps, the position of a sprite,
3
-- and so on. It only updates when visible.
4
--
5
-- This also allows debugging hotkeys -- e.g. you could set it so that
6
-- pressing Control-Alt-I toggles invincibility of the player sprite.
7
-- Out of the box:
8
--		- Control-Alt-F toggles fullscreen
9
--		- Control-Alt-Q quits the app.
10
--		- Control-Alt-P deactivates the view.
11
-- 		- Control-Alt-R reloads all app code from on disk.
12
--		- Control-Alt-S saves a screenshot to the app's directory --
13
--		  see https://love2d.org/wiki/love.filesystem for where this is.
14
15
DebugConsole = Group:extend
16
{
17
	-- Property: toggleKey
18
	-- What key toggles visibility. By default, this is the tab key.
19
	toggleKey = 'tab',
20
21
	-- Property: hotkeyModifiers
22
	-- A table of modifier keys that must be held in order to activate
23
	-- a debugging hotkey (set via <addHotkey()>). If you want hotkeys to
24
	-- activate without having to hold any keys down, set this to nil.
25
	hotkeyModifiers = {'ctrl', 'alt'},
26
27
	-- Property: watchBasics
28
	-- If true, the console will automatically start watching the frames
29
	-- per second and memory usage. Changing this value after the object has
30
	-- been created has no effect.
31
	watchBasics = true,
32
33
	-- Property: watchWidth
34
	-- How wide the sidebar, where watch values are displayed, should be.
35
	watchWidth = 150,
36
37
	-- Property: inputHistory
38
	-- A table of previously-entered commands.
39
	inputHistory = {},
40
41
	-- Property: inputHistoryIndex
42
	-- Which history entry, if any, we are displaying.
43
	inputHistoryIndex = 1,
44
45
	-- Property: bg
46
	-- The background <Fill> used to darken the view.
47
48
	-- Property: log
49
	-- The <Text> sprite showing recent lines in the log.
50
51
	-- Property: watchList
52
	-- The <Text> sprite showing the state of all watched variables.
53
54
	-- Property: input
55
	-- The <TextInput> that the user types into to enter commands.
56
57
	-- Property: prompt
58
	-- The <Text> sprite that shows a > in front of commands.
59
60
	-- internal property: _bindings
61
	-- Keeps track of debugging hotkeys.
62
63
	new = function (self, obj)
64
		local width = the.app.width
65
		local height = the.app.height
66
67
		obj = self:extend(obj)
68
		
69
		obj.visible = false
70
		obj._watches = {}
71
		obj._hotkeys = {}
72
73
		obj.fill = Fill:new{ x = 0, y = 0, width = width, height = height, fill = {0, 0, 0, 200} }
74
		obj:add(obj.fill)
75
76
		obj.log = Text:new{ x = 4, y = 4, width = width - self.watchWidth - 8, height = height - 8, text = '' }
77
		obj:add(obj.log)
78
79
		obj.watchList = Text:new{ x = width - self.watchWidth - 4, y = 4,
80
								   width = self.watchWidth - 8, height = height - 8, text = '', wordWrap = false }
81
		obj:add(obj.watchList)
82
83
		obj.prompt = Text:new{ x = 4, y = 0, width = 100, text = '>' }
84
		obj:add(obj.prompt)
85
86
		local inputIndent = obj.log._fontObj:getWidth('>') + 4
87
		obj.input = TextInput:new
88
		{
89
			x = inputIndent, y = 0, width = the.app.width,
90
			active = false,
91
			onType = function (self, char)
92
				return char ~= the.console.toggleKey
93
			end
94
		}
95
		obj:add(obj.input)
96
97
		-- some default behavior
98
99
		obj:addHotkey('f', function() the.app:toggleFullscreen() end)
100
		obj:addHotkey('p', function()
101
			the.view.active = not the.view.active
102
			if the.view.active then
103
				the.view:tint()
104
			else
105
				the.view:tint(0, 0, 0, 200)
106
			end
107
		end)
108
		obj:addHotkey('q', love.event.quit)
109
		if debugger then obj:addHotkey('r', debugger.reload) end
110
		obj:addHotkey('s', function() the.app:saveScreenshot('screenshot.png') end)
111
		
112
		if obj.watchBasics then
113
			obj:watch('FPS', 'love.timer.getFPS()')
114
			obj:watch('Memory', 'math.floor(collectgarbage("count") / 1024) .. "M"')
115
		end
116
117
		-- hijack print function
118
		-- this is nasty to debug if it goes wrong, be careful
119
120
		obj._oldPrint = print
121
		print = function (...)
122
			local caller = debug.getinfo(2)
123
124
			if caller.linedefined ~= 0 then
125
				obj.log.text = obj.log.text .. '(' .. caller.short_src .. ':' .. caller.linedefined .. ') '
126
			end
127
128
			for _, value in pairs{...} do
129
				obj.log.text = obj.log.text .. tostring(value) .. ' '
130
			end
131
132
			obj.log.text = obj.log.text .. '\n'
133
			obj._updateLog = true
134
			obj._oldPrint(...)
135
		end
136
137
		debugger._unsourcedPrint = function (...)
138
			for _, value in pairs{...} do
139
				obj.log.text = obj.log.text .. tostring(value) .. ' '
140
			end
141
142
			obj.log.text = obj.log.text .. '\n'
143
			obj._updateLog = true
144
			obj._oldPrint(...)
145
		end
146
147
		the.console = obj
148
		if obj.onNew then obj.onNew() end
149
		return obj
150
	end,
151
152
	-- Method: watch
153
	-- Adds an expression to be watched.
154
	--
155
	-- Arguments:
156
	--		label - string label
157
	--		expression - expression to evaluate as a string
158
159
	watch = function (self, label, expression)
160
		table.insert(self._watches, { label = label,
161
									  func = loadstring('return ' .. expression) })
162
	end,
163
164
	-- Method: addHotkey
165
	-- Adds a hotkey to execute a function. This hotkey will require
166
	-- holding down whatever modifiers are set in <hotkeyModifiers>.
167
	--
168
	-- Arguments:
169
	--		key - key to trigger the hotkey
170
	--		func - function to run. This will receive the key that
171
	--			   was pressed, so you can re-use functions (i.e. 
172
	--			   the 1 key loads level 1, the 2 key loads level 2).
173
	--
174
	-- Returns:
175
	--		nothing
176
177
	addHotkey = function (self, key, func)
178
		table.insert(self._hotkeys, { key = key, func = func })
179
	end,
180
181
	-- Method: execute
182
	-- Safely executes a string of code and prints the result.
183
	--
184
	-- Arguments:
185
	--		code - string code to execute
186
	--
187
	-- Returns:
188
	--		string result
189
190
	execute = function (self, code)
191
		if string.sub(code, 1, 1) == '=' then
192
			code = 'debugger._unsourcedPrint (' .. string.sub(code, 2) .. ')'
193
		end
194
195
		local func, err = loadstring(code)
196
197
		if func then
198
			local ok, result = pcall(func)
199
200
			if not ok then
201
				debugger._unsourcedPrint('Error, ' .. tostring(result) .. '\n')
202
			else
203
				debugger._unsourcedPrint('')
204
			end
205
206
			return tostring(result)
207
		else
208
			debugger._unsourcedPrint('Syntax error, ' .. string.gsub(tostring(err), '^.*:', '') .. '\n')
209
		end
210
	end,
211
212
	-- Method: show
213
	-- Shows the debug console.
214
	-- 
215
	-- Arguments:
216
	--		none
217
	--
218
	-- Returns:
219
	--		nothing
220
221
	show = function (self)
222
		self.visible = true
223
		self.input.active = true
224
	end,
225
226
	-- Method: hide
227
	-- Hides the debug console.
228
	-- 
229
	-- Arguments:
230
	--		none
231
	--
232
	-- Returns:
233
	--		nothing
234
235
	hide = function (self)
236
		self.visible = false
237
		self.input.active = false
238
	end,
239
240
	update = function (self, elapsed)
241
		-- listen for visibility key
242
243
		if the.keys:justPressed(self.toggleKey) then
244
			self.visible = not self.visible
245
			self.input.active = self.visible
246
		end
247
248
		-- listen for hotkeys
249
250
		local modifiers = (self.hotkeyModifiers == nil)
251
252
		if not modifiers then
253
			modifiers = true
254
255
			for _, key in pairs(self.hotkeyModifiers) do
256
				if not the.keys:pressed(key) then
257
					modifiers = false
258
					break
259
				end
260
			end
261
		end
262
263
		if modifiers then
264
			for _, hotkey in pairs(self._hotkeys) do
265
				if the.keys:justPressed(hotkey.key) then
266
					hotkey.func(hotkey.key)
267
				end
268
			end
269
		end
270
271
		if self.visible then
272
			-- update watches
273
274
			self.watchList.text = ''
275
			
276
			for _, watch in pairs(self._watches) do
277
				local ok, value = pcall(watch.func)
278
				if not ok then value = nil end
279
280
				self.watchList.text = self.watchList.text .. watch.label .. ': ' .. tostring(value) .. '\n'
281
			end
282
283
			-- update log
284
285
			if self._updateLog then
286
				local lineHeight = self.log._fontObj:getHeight()
287
				local _, height = self.log:getSize()
288
				local linesToDelete = math.ceil((height - the.app.height - 20) / lineHeight)
289
				
290
				if linesToDelete > 0 then
291
					self.log.text = string.gsub(self.log.text, '.-\n', '', linesToDelete) 
292
					height = height - linesToDelete * lineHeight
293
				end
294
295
				self.prompt.y = height + 4
296
				self.input.y = height + 4
297
				self._updateLog = false
298
			end
299
300
			-- handle special keys at the console
301
302
			if the.keys:pressed('ctrl') and the.keys:justPressed('a') then
303
				self.input.caret = 0
304
			end
305
306
			if the.keys:pressed('ctrl') and the.keys:justPressed('e') then
307
				self.input.caret = string.len(self.input.text)
308
			end
309
310
			if the.keys:pressed('ctrl') and the.keys:justPressed('k') then
311
				self.input.caret = 0
312
				self.input.text = ''
313
			end
314
315
			if the.keys:justPressed('up') and self.inputHistoryIndex > 1 then
316
				-- save what the user was in the middle of typing
317
318
				self.inputHistory[self.inputHistoryIndex] = self.input.text
319
320
				self.input.text = self.inputHistory[self.inputHistoryIndex - 1]
321
				self.input.caret = string.len(self.input.text)
322
				self.inputHistoryIndex = self.inputHistoryIndex - 1
323
			end
324
325
			if the.keys:justPressed('down') and self.inputHistoryIndex < #self.inputHistory then
326
				self.input.text = self.inputHistory[self.inputHistoryIndex + 1]
327
				self.input.caret = string.len(self.input.text)
328
				self.inputHistoryIndex = self.inputHistoryIndex + 1
329
			end
330
331
			if the.keys:justPressed('return') then
332
				debugger._unsourcedPrint('>' .. self.input.text)
333
				self:execute(self.input.text)
334
				table.insert(self.inputHistory, self.inputHistoryIndex, self.input.text)
335
336
				while #self.inputHistory > self.inputHistoryIndex do
337
					table.remove(self.inputHistory)
338
				end
339
340
				self.inputHistoryIndex = self.inputHistoryIndex + 1
341
				self.input.text = ''
342
				self.input.caret = 0
343
			end
344
		end
345
346
		Group.update(self, elapsed)
347
	end
348
}
349
350
-- Function: debugger.reload
351
-- Resets the entire app and forces all code to be reloaded from 
352
-- on disk. via https://love2d.org/forums/viewtopic.php?f=3&t=7965
353
-- 
354
-- Arguments:
355
--		none
356
--
357
-- Returns:
358
--		nothing
359
360
if debugger then
361
	debugger.reload = function()
362
		if DEBUG then
363
			love.audio.stop()
364
365
			-- create local references to needed variables
366
			-- because we're about to blow the global scope away
367
368
			local initialGlobals = debugger._initialGlobals
369
			local initialPackages = debugger._initialPackages
370
			
371
			-- reset global scope
372
373
			for key, _ in pairs(_G) do
374
				_G[key] = initialGlobals[key]
375
			end
376
377
			-- reload main file and restart
378
379
			for key, _ in pairs(package.loaded) do
380
				if not initialPackages[key] then
381
					package.loaded[key] = nil
382
				end
383
			end
384
385
			require('main')
386
			love.load()
387
		end
388
	end
389
end