/zoeplat

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