Skip to content

Commit 919f59d

Browse files
committed
feat(log): improved logging system (#39)
1 parent 8e6f2e7 commit 919f59d

File tree

1 file changed

+162
-93
lines changed

1 file changed

+162
-93
lines changed

src/lua/log.lua

Lines changed: 162 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,76 @@
11
local json = require("json")
22
local 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
4628
end
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
-- =============================================================================
@@ -54,13 +80,14 @@ end
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")
6693
end
@@ -70,22 +97,34 @@ end
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")
91130
end
@@ -95,25 +134,21 @@ end
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")
107144
end
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")
119154
end
@@ -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")
141174
end
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")
159190
end
@@ -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")
175204
end
@@ -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")
191218
end
@@ -195,7 +222,7 @@ end
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")
287355
end
288356

357+
---@type Log
289358
return LOG

0 commit comments

Comments
 (0)