Fledgling dev flailing their way through a magical girl apocalypse

Hello, everyone!

I haven’t programmed in 20-some years, so when I got an itch to make a game, I (as one does) picked an engine and language I knew nothing about.

The concept is a cooperative card and adventure game based on my Post Apocalyptic Magical Girls setting. (I’ve written a series of short stories, though most are so far set during the apocalypse.)

To further complicate my life, I intend to integrate the game with chat on my twitch channel.

I spend most of my time poking around examples, the forum, and google searches looking for examples of what I am Trying To Do ™.

After considering the technologies and concepts involved, I’ve split the project into two phases:

Phase 1: creating an application to display various aspects of the game and chat I can capture in an OBS source to show on stream. The intention is to incorporate graphics into the chat for themed view for the channel and game. (And finally show the FFZ mod icon!)

I believe that some aspects of this will be useful for streams in general, so will split off a separate version. I don’t intend the full functionality of a chat bot (there’s many excellent ones already) but instead a useful app to display graphics inside a chat window for capture.

Phase 2: Second phase expands the interface from the first phase to create the game itself. The game’s logic, characters, inventory, chat parsing, and user information. I have a lengthy design document for this phase, but it will likely be defenstrated as soon as the code starts.

So far in my adventure I’ve managed—very little.

Unless one counts hours of searching, reading, and frustration. After fixing some typos and realizing somethings need quotes, I have:

  • a simple interface with a couple buttons which test some HTML functionality.
  • DefSave is used to store/retrieve token and ID info for querying the twitch API for information. (since I have the editor on the stream, I don’t want credentials in the code)
  • a pretty background

Things I learned:

  1. When adding a dependency, use Project → Fetch Libraries immediately. The error of “missing directory/file.lua” means the library isn’t installed. Don’t create directory/file.lua because the library will be hidden when it is downloaded later.
  2. When seeing tutorials/examples that use variables in one spot, and then using a string there in my own code, quotes are important.
  3. If something seems difficult, someone probably wrote a library to make it easier. (example: defsave)

Next up: display the results of the query in a window. I’ve seen many examples of static windows and buttons, but not one with content generated by the function. Back to searches!


Hey! That’s me! Except I’ve been here for 4 years now, learnt a ton of neat stuff and have about a dozen or so projects I really want to finish some day.

You’ll do fine. Good luck!



Exciting and really cool!

Interesting. I think @Alex_8BitSkull did something similar a year or so ago. Ah, yes, here it is: Stream Asteroids (Twitch game)

Yes, this is usually the case! And don’t be afraid of asking for help either!

To quickly move forward I’d recommend that you take a look at the DearImGUI extension (https://github.com/britzl/extension-imgui). It can really help speed up prototyping!

Sounds like me and my writing! 2 novels down, 27 more started!


Ooooh, this looks so nice. Like a fresh pizza! (I might be a bit hungry.)

I came across the thread while researching. Reading the bot log is a good approach, since it alleviates a few issues when dealing with twitch’s chat. And will probably start out with something similar i(n my case, planning to read the log from chatty, a twitch chat client).

Future plans are for the program to respond to some chat commands. I’ve not decided if that will be done graphically/on screen or via chat (there’s pros and cons to both). The latter will require the program to access Twitch’s IRC. I’ve looked at how a few lua-based bots handle IRC and it looks pretty straightforward.

1 Like

Well, 5 days later and I’m still at it!

I spent half the time experimenting with accessing a log file and discovering that doing so 60x a second was Too Much ™. Pesky OS. There was a close statement too, but windows Was Not Happy.

The issue was further complicated by the need to start and stop file reading—the program making the logs rotate them so the filename changes (plan is to have it open the most recent).

The code examples from @Alex_8BitSkull and stack exchange were invaluable. Though I was distracted from the actual reading of the file by an epic struggle with timer.cancel. Creating timers is easy—stopping them is another monster altogether.

Pkeod’s comment in DyleniumFalcon’s thread ( Timer.cancel (solved) ) gave my exhausted brain a much-needed nudge. With that inspiration, time.cancel was finally slain, and laid to rest in an on_message.

Most of which is debugging prints to myself.

local function boom(self, handle, time_elapsed)						-- for testing 
	local functn = "boom "											-- debug 
	print(scriptn .. functn .. "boom handle " .. handle)			-- debug 

function on_message(self, message_id, message, sender)
	-- use on_message to start / stop the log reading
	-- Learn more: https://defold.com/manuals/message-passing/
	local functn = "on_message "									-- for debugging
	if message_id == hash("start_reading") then						-- check if the messages is "start_reading"
		print(scriptn .. functn .. "message_id " .. message_id)		-- debug seemed like useful thing to know.
		read_log = "yes"											-- set read_log to yes. for controlling things.
		print(scriptn .. functn .. "read_log " .. read_log) 		-- debug 
		if self.start_presses <= 0 then 							-- help prevent more timers running.
			self.tail_timer = timer.delay(0.5, true, boom) 			-- wait .5 seconds and call function to read the log.
																	-- repeat until canceled. Took me an hour to figure out timers.
			self.start_presses = self.start_presses + 1				-- help prevent more timers from running. needs to be better 
	if message_id == hash("stop_reading") then						-- check for "stop_reading"
		print(scriptn .. functn .. "message_id " .. message_id)		-- debug seemed like useful thing to know.
		read_log = "no"												-- set read_log to no, for control later 
		print(scriptn .. functn .. "read_log " .. read_log) 		-- debug 
		timer.cancel(self.tail_timer)								-- DIE DIE DIE DIE DIE
																	-- took me 2 days to figure out how to cancel.
		self.start_presses = self.start_presses - 1					-- decriment the timer count. 

Now that the log isn’t clobbered by 1000s of accesses, I can return to reading and doing something with the data.

The obligatory screenshot. I love the background, I’m so glad the artist licensed it. This will eventually turn into a settings and configuration screen, but for now I just make buttons and hope things happen.

On the left is are a couple buttons that use http.request(). One returns the defold home page’s status. The second is more complicated request to twitch’s API that returns data for a user.

On the right is where most of my efforts currently lay, with nothing much happening (at least not in the game UI, there’s loads in the console).

Behold, the glory of all my debug messages!

DEBUG:SCRIPT: main on_input Touch!
DEBUG:SCRIPT: test_http.gui_script stop reading log
DEBUG:SCRIPT: readchatlog on_message message_id [stop_reading]
DEBUG:SCRIPT: readchatlog on_message read_log no
DEBUG:SCRIPT: readchatlog log_timer usetimer no
DEBUG:SCRIPT: main on_input Touch!
DEBUG:SCRIPT: test_http.gui_script start reading
DEBUG:SCRIPT: readchatlog on_message message_id [start_reading]
DEBUG:SCRIPT: readchatlog on_message read_log yes
DEBUG:SCRIPT: readchatlog boom boom handle 65536
DEBUG:SCRIPT: main on_input Touch!
DEBUG:SCRIPT: test_http.gui_script stop reading log
DEBUG:SCRIPT: readchatlog on_message message_id [stop_reading]
DEBUG:SCRIPT: readchatlog on_message read_log no
DEBUG:SCRIPT: readchatlog log_timer usetimer no

Next up:

  • Reading the file
  • separating out the user names from the message
  • throwing it into a db
  • should probably make a db too
  • figuring out how to display it. There’s a particular UI I want to build.

The goal is to have each log line have a rectangle. The next line will bump the previous rectangles up/down until the oldest messages leave the viewing area.


Progress has been made!

  • reading the file :white_check_mark:
  • separating out the user names from the message :white_check_mark:

In preparation for further error messa–I mean, progress–each line now has a UID (date to the second and the position in the file it was read–if it isn’t unique Something Went Wrong™). Username has been stripped of surrounding <>'s and symbols. And look, there’s the rest of the message!

The code was very hacky and I’m sure it can be streamlined considerably. I also experimented with functions. They’re my little buddies from a past era.

Gotta love the little guys!

-- https://stackoverflow.com/questions/42062779/convert-string-to-timestamp
-- https://www.lua.org/pil/22.1.html
-- Assuming a date pattern like: yyyy-mm-dd hh:mm:ss
local function grab_time(raw_message)
	local functn = "grab_time "										-- debug 
	local raw_time = string.sub(raw_message, 2, 21)					-- msg_time = from character 2 to 21
	local raw_time = raw_time:gsub("[^%w%s]+", "")					-- remove non-alphanumerics (still needs work) 
	local msg_time = raw_time:gsub("%s+", "")						-- remove the spaces 
	print(scriptn .. functn .. "msg_time " .. msg_time)				-- debug 
	return msg_time 												-- return the time string -- can this be combined with above?

-- [2022-03-28 14:33:08] <@PokemonCommunityGame> Abra has been caught by: wyrdewyn
-- 1---------------------22  ------------------- I did a lot of counting.
local function grab_user(raw_message)
	local functn = "grab_user "
	local end_of_name_area = 3
	local msg_after_date = string.sub(raw_message, 23)						-- grab message after the date.
	if string.find(msg_after_date, "<", 1, true) == 1 then					-- if the first character is a < then it is a user name
		local end_of_name_area = string.find(msg_after_date, ">", 2, true)	-- find out how many characters it takes to get to >
		local user_name_area = string.sub(msg_after_date, 1, end_of_name_area)	-- snip out the letters between
		local cleaned_name = user_name_area:gsub("[^%w%s]+", "")
		print(scriptn .. functn .. "cleaned_name " .. cleaned_name)
		return cleaned_name
		return "SYSTEM"
-- https://stackoverflow.com/questions/62647752/lua-string-drop-non-alphanumeric-or-space
-- customer_input , _ = customer_input:gsub("[^%w%s]+", "");
-- https://stackoverflow.com/questions/12117965/lua-how-to-check-if-a-string-contains-only-numbers-and-letters
-- https://stackoverflow.com/questions/10460126/how-to-remove-spaces-from-a-string-in-lual

local function grab_body(raw_message)
	local functn = "grab_body "
	if string.find(raw_message, "<", 1, true) == 23 then 					-- if < at 23, then start of a username.
		local end_of_name_area = string.find(raw_message, ">", 24, true) + 1	-- find end of the user name.
		print(scriptn .. functn .. "end_of_name_area: " .. end_of_name_area)	-- debug 
		local msg_body = string.sub(raw_message, end_of_name_area)			-- after username is the message.
		print(scriptn .. functn .. "msg_body " .. msg_body)
		return msg_body
		local msg_body = string.sub(raw_message, 23)						-- no < means a status message.
		return msg_body 

the check_log() function grew a bit, but most of it is debug messages.

local function check_log(self, handle)										-- the master function for log reading.
	local functn = "check_log "
	if log_accessed == 0 then											-- should only be donce once, when timer first activates.
		-- file_length = check_file_size(filename)						-- check what the file length is when first opened.
		local logfile = io.input(filename, "r")							-- access in read mode 
		file_length = logfile:seek("end")								-- length of file when first accssed 
		print(scriptn .. functn .. "file_length " .. file_length) 		-- debug 
		--		log_accessed = 0
		log_accessed = log_accessed + 1									-- increment and hopfully not read the same line again 
		print(scriptn .. functn .. "log_accessed 1 " .. log_accessed)	-- debug 
		--		length_alpha = check_file_size(filename)				-- log_accessed lets me reuse the function 
		local logfile = io.input(filename, "r")							-- access in read mode 
		length_alpha = logfile:seek("end")								-- length of file when first accssed 
		-- print(scriptn .. functn .. "file_length 2 " .. file_length) 	-- debug 
		--		log_accessed = 0
		log_accessed = log_accessed + 1									-- to prevent an update to file size too soon 
		-- print(scriptn .. functn .. "log_accessed 2 " .. log_accessed)	-- debug 
		if length_alpha > file_length then 								-- if the file size is larger, there's new lines to read 
			-- tail_log(length_alpha)									-- function to do the reading 
			--															-- testing the log print.
			-- local logfile = io.input(filename, "r")					-- done above, don't need?
			logfile:seek("set", file_length)							-- "set" the "pointer" to file_length
			if logfile then												-- double check it is not "nil"
				for i=1,seek_lines do									-- start iterating though a possible number of lines (some might have been added)
					local log_lines = io.read("*l")						-- I thnk this reads by "next line to "end of line". If doesn't do what I want, then there's the line() call to try.
					file_length = logfile:seek()						-- set file length to the new EOF?
					if not log_lines then								-- if log_lines is empty, stop 
						break											-- stop reading
					print(scriptn .. functn .. "log lines" .. log_lines)	-- debug & print the log_lines 
					-- figure out the message UUID. Timestamp + file_length will be plenty specific.
					raw_msg = log_lines
					local msg_time = grab_time(raw_msg)
					local msg_user = grab_user(raw_msg)
					local msg_body = grab_body(raw_msg)
					local msg_uid = msg_time .. "-" .. file_length				-- not sure I want this to be a number so using a dash
					--msg.post("go#test_http", "log_parsed", {line = log_lines}) 				-- send messge to the GUI node.
					print(msg_uid .. "##".. msg_user .. "##" .. msg_body)
					msg.post("go#test_http", "log_message", {uid = msg_uid, user = msg_user, body = msg_body}) -- send messge to the GUI node.
			file_length = length_alpha									-- after reading, increase the stored filesize.
			-- print(scriptn .. functn .. "file_length 3 " .. file_length) -- debug 

Meanwhile, back in the gui_script there is a little friend in the on_messages

function on_message(self, message_id, message, sender)
	if message_id == hash("log_parsed") then									-- check message ID 
		local log_window_node = gui.get_node("text_log_window")					-- get the node info 
		gui.set_text(log_window_node, message.line)								-- print message to text node 
	if message_id == hash("log_message") then									-- check message ID 
		local log_window_node = gui.get_node("text_log_window")					-- get node info 
		local glue_message = message.uid .. ": " .. message.user .. ": " .. message.body	-- our new friend from readchatlog 
						-- {uid = msg_uid, user = msg_user, body = msg_body}	-- my memory is terrible 
		gui.set_text(log_window_node, glue_message)								-- print message to text node 
	end																			-- go, little buddy, go! 

I think I figured out how to get more than one message to show up at a time: duct tape!


We have multiple lines!

It was both easier and more difficult than I thought. My original plan was to build a table with reference numbers and then cycle through what was displayed with fun and math. Then I discovered table.insert included references, and table.remove happily removes the lowest. Talk about a time saver!

Then I spent the next few hours running in circles between strings table string table table not a table a string in a table table no not that table table table string untangle the string of tables!

Trying to figure out where a string ended up a table ended up a string was like

Also, table.concat doesn’t like new lines, but that’s fine, I put them in the strings near the beginning instead.


Then with a little fiddling and guessing, the table of strings played nicely with concat and

In the end, 6 functions and dozens of lines of code and debug messages became… ducttape.script!

-- initial variables


So, my thoughts on this.
Need to display multiple lines.
So build a series of text messages and glue them together?
* get the line from "reachchatlog.script" (as a message)
* put line in an array
* format array as a single opbject to be displayed?
* send object to test_http.gui for display
* wait for next line 
* older lines need removed
* add to array and reformat
* send new array to "test_http.gui"


local scriptn = "ducttape.script "										-- debug 
local functn = "functn "												-- debug 
local message_table_lines = 10											-- show last 10 lines from log 
local message_table = {}												-- behold! A table!

function init(self)
--	self.message_table = {}

function on_message(self, message_id, message, sender)
	local functn = "on_message "
	if message_id == hash("log_message") then										-- check the message name 
		local taped_message = message.user .. ": ".. message.body .. "\n"			-- put it together. don't use {} here.
		local ducttaped = taped_message												-- I really wanted to use ducttaped.
--			print(scriptn .. functn .. "taped_message " .. taped_message)			-- debug 
		table.insert(message_table, ducttaped)										-- put string in table at pos n+1
--			print(scriptn .. functn .. "message stored I hope")						-- debug 
		local table_entries = table.maxn(message_table)								-- see how many entries are in the table 
--			print(scriptn .. functn .. "table entries: " .. table_entries)			-- debug 
		if table_entries >= message_table_lines then								-- if there are more entires than X
			table.remove(message_table, 1)											-- remove first entry 
		--	print(scriptn .. functn .. "removed message")							-- debug 
			print(scriptn .. functn .. "no messages to remove, works fine to here.")	-- debug 
		if table.maxn(message_table) >=1 then										-- if there is a message
			message_to_send = table.concat(message_table)
			msg.post("go#test_http", "new_message_table", {table = message_to_send}) -- send messge to the GUI script 
--			print(scriptn .. functn .. "sent message to test_http")					-- debug 
			print(scriptn .. functn .. " nil table")								-- debug 

I also went through the other scripts and commented out debug prints. 30 lines of debugging for one line of text was getting silly. (:

Of special help in today’s adventure was Egor Skriptunoff’s comment in LUA - invalid value (table) at index 1 in table for 'concat' - Stack Overflow

And some comments here Table binding (passing tables as reference) - #3 by Vik and Table binding (passing tables as reference) - #3 by Vik

And now, for tomorrow!