11local json = require (" json" )
22local socket = require (" socket" )
33
4- LOG = {}
5- LOG .mod_path = nil
6- LOG .current_run_file = nil
4+ LOG = {
5+ mod_path = nil ,
6+ current_run_file = nil ,
7+ pending_logs = {},
8+ previous_game_state = nil ,
9+ }
710
811-- =============================================================================
912-- Utility Functions
1013-- =============================================================================
1114
12- --- Generates an ISO 8601 timestamp for filename
13- --- @return string ISO 8601 timestamp in format YYYYMMDDTHHMMSS
14- function LOG .generate_iso8601_timestamp ()
15- return tostring (os.date (" !%Y%m%dT%H%M%S" ))
16- end
17-
18- --- Logs a function call to the JSONL file
19- --- @param function_name string The name of the function being called
20- --- @param arguments table The parameters passed to the function
21- function LOG .write (function_name , arguments )
22- --- @type LogEntry
23- local log_entry = {
24- timestamp_ms = math.floor (socket .gettime () * 1000 ),
25- [" function" ] = {
26- name = function_name ,
27- arguments = arguments ,
28- },
29- -- game_state before the function call
30- game_state = utils .get_game_state (),
31- }
32- sendDebugMessage (" Writing log entry: " .. utils .table_to_json (log_entry , 4 ), " LOG" )
33- local log_line = json .encode (log_entry ) .. " \n "
34-
35- local log_file_path
15+ --- Writes a log entry to the JSONL file
16+ --- @param log_entry table The log entry to write
17+ local function write_log_entry (log_entry )
3618 if LOG .current_run_file then
37- log_file_path = LOG . current_run_file
38- local file = io.open (log_file_path , " a" )
19+ local log_line = json . encode ( log_entry ) .. " \n "
20+ local file = io.open (LOG . current_run_file , " a" )
3921 if file then
4022 file :write (log_line )
4123 file :close ()
4224 else
43- sendErrorMessage (" Failed to open log file for writing: " .. log_file_path , " LOG" )
25+ sendErrorMessage (" Failed to open log file for writing: " .. LOG . current_run_file , " LOG" )
4426 end
4527 end
4628end
4729
30+ --- Processes pending logs by checking completion conditions
31+ function LOG .update ()
32+ for key , pending_log in pairs (LOG .pending_logs ) do
33+ if pending_log .condition () then
34+ pending_log .log_entry [" game_state_after" ] = utils .get_game_state ()
35+ write_log_entry (pending_log .log_entry )
36+ LOG .previous_game_state = pending_log .log_entry .game_state_after
37+ LOG .pending_logs [key ] = nil
38+ end
39+ end
40+ end
41+
42+ --- Logs a function call to the JSONL file with completion waiting
43+ --- @param original_function function The original function being called
44+ --- @param function_call FunctionCall The function call to log
45+ function LOG .write (original_function , function_call , ...)
46+ local log_entry = {
47+ timestamp_ms = math.floor (socket .gettime () * 1000 ),
48+ [" function" ] = function_call ,
49+ game_state_before = LOG .previous_game_state ,
50+ }
51+
52+ local result = original_function (... )
53+ sendInfoMessage (function_call .name .. " (" .. json .encode (function_call .arguments ) .. " )" , " LOG" )
54+
55+ -- Get completion condition by function name
56+ local condition = utils .COMPLETION_CONDITIONS [function_call .name ]
57+ if condition then
58+ -- Create a unique key for the pending log
59+ local pending_key = function_call .name .. " _" .. tostring (socket .gettime ())
60+ LOG .pending_logs [pending_key ] = {
61+ log_entry = log_entry ,
62+ condition = condition ,
63+ }
64+ else
65+ -- Immediate logging if no condition found
66+ log_entry [" game_state_after" ] = utils .get_game_state ()
67+ write_log_entry (log_entry )
68+ LOG .previous_game_state = log_entry .game_state_after
69+ end
70+
71+ return result
72+ end
73+
4874-- =============================================================================
4975-- Hooks
5076-- =============================================================================
5480-- -----------------------------------------------------------------------------
5581
5682--- Hooks into G.FUNCS.go_to_menu
57- function LOG . hook_go_to_menu ()
83+ function hook_go_to_menu ()
5884 local original_function = G .FUNCS .go_to_menu
59- G .FUNCS .go_to_menu = function (args )
60- local arguments = {}
61- local name = " go_to_menu"
62- LOG .write (name , arguments )
63- return original_function (args )
85+ G .FUNCS .go_to_menu = function (...)
86+ local function_call = {
87+ name = " go_to_menu" ,
88+ arguments = {},
89+ }
90+ return LOG .write (original_function , function_call , ... )
6491 end
6592 sendDebugMessage (" Hooked into G.FUNCS.go_to_menu for logging" , " LOG" )
6693end
7097-- -----------------------------------------------------------------------------
7198
7299--- Hooks into G.FUNCS.start_run
73- function LOG . hook_start_run ()
100+ function hook_start_run ()
74101 local original_function = G .FUNCS .start_run
75102 G .FUNCS .start_run = function (game_state , args )
76103 -- Generate new log file for this run
77- local timestamp = LOG .generate_iso8601_timestamp ()
78- LOG .current_run_file = LOG .mod_path .. " runs/" .. timestamp .. " .jsonl"
79- sendInfoMessage (" Starting new run log: " .. timestamp .. " .jsonl" , " LOG" )
80- local arguments = {
81- deck = G .GAME .selected_back .name ,
82- stake = args .stake ,
83- seed = args .seed ,
84- challenge = args .challenge and args .challenge .name ,
104+ if args .log_path then
105+ local file = io.open (args .log_path , " r" )
106+ if file then
107+ file :close ()
108+ sendErrorMessage (" Log file already exists, refusing to overwrite: " .. args .log_path , " LOG" )
109+ return
110+ end
111+ LOG .current_run_file = args .log_path
112+ sendInfoMessage (" Starting new run log: " .. args .log_path , " LOG" )
113+ else
114+ local timestamp = tostring (os.date (" !%Y%m%dT%H%M%S" ))
115+ LOG .current_run_file = LOG .mod_path .. " runs/" .. timestamp .. " .jsonl"
116+ sendInfoMessage (" Starting new run log: " .. timestamp .. " .jsonl" , " LOG" )
117+ end
118+ local function_call = {
119+ name = " start_run" ,
120+ arguments = {
121+ deck = G .GAME .selected_back .name ,
122+ stake = args .stake ,
123+ seed = args .seed ,
124+ challenge = args .challenge and args .challenge .name ,
125+ },
85126 }
86- local name = " start_run"
87- LOG .write (name , arguments )
88- return original_function (game_state , args )
127+ return LOG .write (original_function , function_call , game_state , args )
89128 end
90129 sendDebugMessage (" Hooked into G.FUNCS.start_run for logging" , " LOG" )
91130end
95134-- -----------------------------------------------------------------------------
96135
97136--- Hooks into G.FUNCS.select_blind
98- function LOG . hook_select_blind ()
137+ function hook_select_blind ()
99138 local original_function = G .FUNCS .select_blind
100139 G .FUNCS .select_blind = function (args )
101- local arguments = { action = " select" }
102- local name = " skip_or_select_blind"
103- LOG .write (name , arguments )
104- return original_function (args )
140+ local function_call = { name = " skip_or_select_blind" , arguments = { action = " select" } }
141+ return LOG .write (original_function , function_call , args )
105142 end
106143 sendDebugMessage (" Hooked into G.FUNCS.select_blind for logging" , " LOG" )
107144end
108145
109146--- Hooks into G.FUNCS.skip_blind
110- function LOG . hook_skip_blind ()
147+ function hook_skip_blind ()
111148 local original_function = G .FUNCS .skip_blind
112149 G .FUNCS .skip_blind = function (args )
113- local arguments = { action = " skip" }
114- local name = " skip_or_select_blind"
115- LOG .write (name , arguments )
116- return original_function (args )
150+ local function_call = { name = " skip_or_select_blind" , arguments = { action = " skip" } }
151+ return LOG .write (original_function , function_call , args )
117152 end
118153 sendDebugMessage (" Hooked into G.FUNCS.skip_blind for logging" , " LOG" )
119154end
@@ -123,37 +158,33 @@ end
123158-- -----------------------------------------------------------------------------
124159
125160--- Hooks into G.FUNCS.play_cards_from_highlighted
126- function LOG . hook_play_cards_from_highlighted ()
161+ function hook_play_cards_from_highlighted ()
127162 local original_function = G .FUNCS .play_cards_from_highlighted
128- G .FUNCS .play_cards_from_highlighted = function (args )
163+ G .FUNCS .play_cards_from_highlighted = function (... )
129164 local cards = {}
130165 for i , card in ipairs (G .hand .cards ) do
131166 if card .highlighted then
132167 table.insert (cards , i - 1 ) -- Adjust for 0-based indexing
133168 end
134169 end
135- local arguments = { action = " play_hand" , cards = cards }
136- local name = " play_hand_or_discard"
137- LOG .write (name , arguments )
138- return original_function (args )
170+ local function_call = { name = " play_hand_or_discard" , arguments = { action = " play_hand" , cards = cards } }
171+ return LOG .write (original_function , function_call , ... )
139172 end
140173 sendDebugMessage (" Hooked into G.FUNCS.play_cards_from_highlighted for logging" , " LOG" )
141174end
142175
143176--- Hooks into G.FUNCS.discard_cards_from_highlighted
144- function LOG . hook_discard_cards_from_highlighted ()
177+ function hook_discard_cards_from_highlighted ()
145178 local original_function = G .FUNCS .discard_cards_from_highlighted
146- G .FUNCS .discard_cards_from_highlighted = function (args )
179+ G .FUNCS .discard_cards_from_highlighted = function (... )
147180 local cards = {}
148181 for i , card in ipairs (G .hand .cards ) do
149182 if card .highlighted then
150183 table.insert (cards , i - 1 ) -- Adjust for 0-based indexing
151184 end
152185 end
153- local arguments = { action = " discard" , cards = cards }
154- local name = " play_hand_or_discard"
155- LOG .write (name , arguments )
156- return original_function (args )
186+ local function_call = { name = " play_hand_or_discard" , arguments = { action = " discard" , cards = cards } }
187+ return LOG .write (original_function , function_call , ... )
157188 end
158189 sendDebugMessage (" Hooked into G.FUNCS.discard_cards_from_highlighted for logging" , " LOG" )
159190end
@@ -163,13 +194,11 @@ end
163194-- -----------------------------------------------------------------------------
164195
165196--- Hooks into G.FUNCS.cash_out
166- function LOG . hook_cash_out ()
197+ function hook_cash_out ()
167198 local original_function = G .FUNCS .cash_out
168- G .FUNCS .cash_out = function (args )
169- local arguments = {}
170- local name = " cash_out"
171- LOG .write (name , arguments )
172- return original_function (args )
199+ G .FUNCS .cash_out = function (...)
200+ local function_call = { name = " cash_out" , arguments = {} }
201+ return LOG .write (original_function , function_call , ... )
173202 end
174203 sendDebugMessage (" Hooked into G.FUNCS.cash_out for logging" , " LOG" )
175204end
@@ -179,13 +208,11 @@ end
179208-- -----------------------------------------------------------------------------
180209
181210--- Hooks into G.FUNCS.toggle_shop
182- function LOG . hook_toggle_shop ()
211+ function hook_toggle_shop ()
183212 local original_function = G .FUNCS .toggle_shop
184- G .FUNCS .toggle_shop = function (args )
185- local arguments = { action = " next_round" }
186- local name = " shop"
187- LOG .write (name , arguments )
188- return original_function (args )
213+ G .FUNCS .toggle_shop = function (...)
214+ local function_call = { name = " shop" , arguments = { action = " next_round" } }
215+ return LOG .write (original_function , function_call , ... )
189216 end
190217 sendDebugMessage (" Hooked into G.FUNCS.toggle_shop for logging" , " LOG" )
191218end
195222-- -----------------------------------------------------------------------------
196223
197224--- Hooks into CardArea:align_cards for hand reordering detection
198- function LOG . hook_hand_rearrange ()
225+ function hook_hand_rearrange ()
199226 local original_function = CardArea .align_cards
200227 local previous_order = {}
201228 CardArea .align_cards = function (self , ...)
@@ -241,11 +268,39 @@ function LOG.hook_hand_rearrange()
241268 cards [pos ] = lookup [card_id ]
242269 end
243270
244- LOG .write (" rearrange_hand" , { cards = cards })
271+ local function_call = {
272+ name = " rearrange_hand" ,
273+ arguments = { cards = cards },
274+ }
275+
276+ --- NOTE: we cannot use LOG.write because we do not have access to game_state_before. We need to recreate it.
277+ --- The following lines corresponds to LOG.write
278+
279+ local log_entry = {
280+ timestamp_ms = math.floor (socket .gettime () * 1000 ),
281+ [" function" ] = function_call ,
282+ game_state_before = previous_game_state ,
283+ game_state_after = utils .get_game_state (),
284+ }
285+ LOG .previous_game_state = log_entry .game_state_after
286+
287+ sendInfoMessage (function_call .name .. " (" .. json .encode (function_call .arguments ) .. " )" , " LOG" )
288+
289+ if LOG .current_run_file then
290+ local log_line = json .encode (log_entry ) .. " \n "
291+ local file = io.open (LOG .current_run_file , " a" )
292+ if file then
293+ file :write (log_line )
294+ file :close ()
295+ else
296+ sendErrorMessage (" Failed to open log file for writing: " .. LOG .current_run_file , " LOG" )
297+ end
298+ end
245299 end
246300 end
247301
248302 previous_order = current_order
303+ previous_game_state = utils .get_game_state ()
249304 return result
250305 else
251306 -- For non-hand card areas, just call the original function
@@ -272,18 +327,32 @@ function LOG.init()
272327 return
273328 end
274329
330+ -- Hook into the API update loop to process pending logs
331+ if API and API .update then
332+ local original_api_update = API .update
333+ --- @diagnostic disable-next-line : duplicate-set-field
334+ API .update = function (dt )
335+ original_api_update (dt )
336+ LOG .update ()
337+ end
338+ sendDebugMessage (" Hooked into API.update for pending log processing" , " LOG" )
339+ else
340+ sendErrorMessage (" API not available - pending log processing disabled" , " LOG" )
341+ end
342+
275343 -- Init hooks
276- LOG . hook_go_to_menu ()
277- LOG . hook_start_run ()
278- LOG . hook_select_blind ()
279- LOG . hook_skip_blind ()
280- LOG . hook_play_cards_from_highlighted ()
281- LOG . hook_discard_cards_from_highlighted ()
282- LOG . hook_cash_out ()
283- LOG . hook_toggle_shop ()
284- LOG . hook_hand_rearrange ()
344+ hook_go_to_menu ()
345+ hook_start_run ()
346+ hook_select_blind ()
347+ hook_skip_blind ()
348+ hook_play_cards_from_highlighted ()
349+ hook_discard_cards_from_highlighted ()
350+ hook_cash_out ()
351+ hook_toggle_shop ()
352+ hook_hand_rearrange ()
285353
286354 sendInfoMessage (" Logger initialized" , " LOG" )
287355end
288356
357+ --- @type Log
289358return LOG
0 commit comments