-- Pokemon Emerald Battle Logger (Final Headless Edition) -- Status: CLEANED. GUI removed. -- Features: Timestamped Logs, Streak Counter, Ordered Reports, Wipeout Safety. -- 1. DYNAMIC PATH FINDING & FILENAME GENERATION function get_script_path() local info = debug.getinfo(1, "S") local path = info.source path = string.sub(path, 2) local dir = path:match("(.*[/\\])") return dir or "" end local timestamp = os.date("%Y%m%d_%H%M") local LOG_FILE = get_script_path() .. "battle_log_" .. timestamp .. ".txt" -- 2. INITIALIZE LOG FILE local f_reset = io.open(LOG_FILE, "w") if f_reset then f_reset:write("--- NEW SESSION: " .. timestamp .. " ---\n") f_reset:close() else LOG_FILE = "battle_log.txt" end -- 3. CONSTANTS local PARTY_START = 0x020244EC local SLOT_SIZE = 100 local OFFSET_NAME = 8 local OFFSET_HP = 86 local OFFSET_MAX_HP = 88 local CHAR_MAP = { [0x00]=" ", [0xA1]="0", [0xA2]="1", [0xA3]="2", [0xA4]="3", [0xA5]="4", [0xA6]="5", [0xA7]="6", [0xA8]="7", [0xA9]="8", [0xAA]="9", [0xAB]="!", [0xAC]="?", [0xAD]=".", [0xAE]="-", [0xB0]="...", [0xB1]="“", [0xB2]="”", [0xB3]="‘", [0xB4]="'", [0xB5]="m", [0xB6]="f", [0xB8]=",", [0xBA]="/", [0xBB]="A", [0xBC]="B", [0xBD]="C", [0xBE]="D", [0xBF]="E", [0xC0]="F", [0xC1]="G", [0xC2]="H", [0xC3]="I", [0xC4]="J", [0xC5]="K", [0xC6]="L", [0xC7]="M", [0xC8]="N", [0xC9]="O", [0xCA]="P", [0xCB]="Q", [0xCC]="R", [0xCD]="S", [0xCE]="T", [0xCF]="U", [0xD0]="V", [0xD1]="W", [0xD2]="X", [0xD3]="Y", [0xD4]="Z", [0xD5]="a", [0xD6]="b", [0xD7]="c", [0xD8]="d", [0xD9]="e", [0xDA]="f", [0xDB]="g", [0xDC]="h", [0xDD]="i", [0xDE]="j", [0xDF]="k", [0xE0]="l", [0xE1]="m", [0xE2]="n", [0xE3]="o", [0xE4]="p", [0xE5]="q", [0xE6]="r", [0xE7]="s", [0xE8]="t", [0xE9]="u", [0xEA]="v", [0xEB]="w", [0xEC]="x", [0xED]="y", [0xEE]="z", [0xFF]="" } -- 4. STATE VARIABLES local last_text_hash = "" local current_turn = 1 local active_name = "My Pokemon" local battle_count = 0 local turn_header_printed = false local actions_this_turn = 0 local battle_over_lock = false -- Memory Pointers local current_hp_addr = 0 local current_max_addr = 0 local party_map = {} -- HP Variables local tracked_hp = -1 local tracked_max_hp = -1 local hp_snapshot_timer = 0 local SNAPSHOT_DELAY = 45 -- Text Variables local stable_text_counter = 0 local potential_text = "" local last_written_line = "" local frames_since_write = 0 local DUPLICATE_COOLDOWN = 20 local battle_trigger_timer = 0 -- Intel local enemy_intel = {} local enemy_order = {} -- 5. HELPERS (UNIVERSAL MEMORY READER) function read_byte_univ(addr) if emu then return emu:read8(addr) elseif memory then return memory.readbyte(addr) end return 0 end function decode_string(address, max_len) local s = "" local i = 0 while i < max_len do local val = read_byte_univ(address + i) if val == 0xFF then break end if val == 0xFE then s = s .. " "; i = i + 1 elseif val == 0xFB or val == 0xFD then i = i + 1 elseif val == 0xFC then i = i + 3 else local char = CHAR_MAP[val] if char then s = s .. char end i = i + 1 end end return s:gsub("^%s*(.-)%s*$", "%1") end function scan_party() party_map = {} for i = 0, 5 do local slot_base = PARTY_START + (i * SLOT_SIZE) local name_addr = slot_base + OFFSET_NAME local raw_name = decode_string(name_addr, 10) if raw_name ~= "" and raw_name ~= nil then local hp_addr = slot_base + OFFSET_HP local max_addr = slot_base + OFFSET_MAX_HP party_map[raw_name] = {hp = hp_addr, max = max_addr} if i == 0 and active_name == "My Pokemon" then active_name = raw_name current_hp_addr = hp_addr current_max_addr = max_addr end end end end -- Robust Wipeout Check function check_player_wipeout() local all_fainted = true local has_pokemon = false for i = 0, 5 do local slot_base = PARTY_START + (i * SLOT_SIZE) local max_hp = read_byte_univ(slot_base + OFFSET_MAX_HP) + (read_byte_univ(slot_base + OFFSET_MAX_HP + 1) * 256) if max_hp > 0 then has_pokemon = true local cur_hp = read_byte_univ(slot_base + OFFSET_HP) + (read_byte_univ(slot_base + OFFSET_HP + 1) * 256) if cur_hp > 0 then all_fainted = false break end end end return (has_pokemon and all_fainted) end function find_memory_slot(text_name) if party_map[text_name] then return party_map[text_name] end for mem_name, data in pairs(party_map) do if string.find(mem_name, text_name) then return data end end return nil end function write_to_file(text) if not text then return end console:log(text) local f = io.open(LOG_FILE, "a") if f then f:write(text .. "\n"); f:close() end end function log_game_text(text) if text == last_written_line then if frames_since_write < DUPLICATE_COOLDOWN then return end end write_to_file(text) last_written_line = text frames_since_write = 0 end function log_damage(text) write_to_file(text) end function is_valid_text(text) if text == nil then return false end if text:match("^%s*$") then return false end return true end function update_hp_tracker() if current_hp_addr == 0 or tracked_hp == -1 then return end local hp_low = read_byte_univ(current_hp_addr) local hp_high = read_byte_univ(current_hp_addr + 1) local current_hp = hp_low + (hp_high * 256) local max_low = read_byte_univ(current_max_addr) local max_high = read_byte_univ(current_max_addr + 1) local max_hp = max_low + (max_high * 256) if tracked_max_hp ~= -1 and max_hp ~= tracked_max_hp then return end local diff = tracked_hp - current_hp if diff > 0 and max_hp > 0 then local pct = (diff / max_hp) * 100 if pct > 0 then log_damage("(" .. active_name .. " lost " .. string.format("%.1f", pct) .. "% of its health!)") end end tracked_hp = current_hp hp_snapshot_timer = 0 end -- INTEL LOGIC function is_enemy_move(name_in_text) if name_in_text == active_name then return false end return true end function register_enemy(name) if not enemy_intel[name] then enemy_intel[name] = {} table.insert(enemy_order, name) end end function parse_enemy_arrival(text) if not text then return end local sent_out = text:match("sent out%s+(.-)[!%.]") if sent_out then register_enemy(sent_out) end local wild_app = text:match("Wild%s+(.-)%s+appeared") if wild_app then register_enemy(wild_app) end end function parse_move_usage(text) if not text then return end local name, move = text:match("^(.-)%s+used%s+(.-)[!%.]") if name and move then name = name:gsub("The wild ", ""):gsub("Foe ", "") if is_enemy_move(name) then register_enemy(name) local current_count = enemy_intel[name][move] or 0 enemy_intel[name][move] = current_count + 1 end end end function save_intel_to_log(header_text) if #enemy_order == 0 then return end local header = header_text or "--- BATTLE REPORT (Enemy Intel) ---" write_to_file("\n" .. header) for _, mon_name in ipairs(enemy_order) do local moves = enemy_intel[mon_name] write_to_file("Pokemon: " .. mon_name) if next(moves) ~= nil then for move_name, count in pairs(moves) do write_to_file(" - " .. move_name .. " (" .. count .. "x)") end end end write_to_file("-----------------------------------") end -- 6. TEXT LOGIC function should_log_line(text) if not is_valid_text(text) then return false end if string.find(text, "What will") then return false end if string.find(text, "TYPE/") or string.find(text, "PP") then return false end if string.find(text, "Battle started") or string.find(text, "appeared!") or string.find(text, "would like to battle") then return true end local last_char = string.sub(text, -1) return (last_char == "." or last_char == "!" or last_char == "?" or last_char == "”") end function check_turn_logic(text) if not text then return end if string.find(text, "appeared!") or string.find(text, "Battle started") or string.find(text, "would like to battle") then if battle_trigger_timer == 0 then battle_count = battle_count + 1 log_game_text("\n__________________________________") log_game_text("NEW BATTLE DETECTED - BATTLE " .. battle_count) battle_trigger_timer = 300 current_turn = 1 turn_header_printed = false actions_this_turn = 0 battle_over_lock = false tracked_hp = -1 tracked_max_hp = -1 hp_snapshot_timer = 0 last_written_line = "" enemy_intel = {} enemy_order = {} scan_party() end return end if active_name and string.find(text, active_name .. " fainted") then update_hp_tracker() tracked_hp = -1 end local new_name = nil local match = text:match("Go!%s+(.-)[!%.]") if match then new_name = match end if not new_name then match = text:match("Do it!%s+(.-)[!%.]"); if match then new_name = match end end if not new_name then match = text:match("Go for it,%s+(.-)[!%.]"); if match then new_name = match end end if not new_name then match = text:match("Your turn,%s+(.-)[!%.]"); if match then new_name = match end end if not new_name then match = text:match("Get 'em,%s+(.-)[!%.]"); if match then new_name = match end end if new_name then active_name = new_name tracked_hp = -1 tracked_max_hp = -1 hp_snapshot_timer = 0 local slot = find_memory_slot(active_name) if slot then current_hp_addr = slot.hp current_max_addr = slot.max else scan_party() slot = find_memory_slot(active_name) if slot then current_hp_addr = slot.hp current_max_addr = slot.max end end end if string.find(text, "What will") then if actions_this_turn > 0 then update_hp_tracker() save_intel_to_log("--- END OF TURN " .. current_turn .. " REPORT ---") current_turn = current_turn + 1 turn_header_printed = false actions_this_turn = 0 end end local is_action = false if string.find(text, "used") then is_action = true end if string.find(text, "is tightening its focus") or string.find(text, "is fast asleep") or string.find(text, "woke up") or string.find(text, "is frozen solid") or string.find(text, "is paralyzed") or string.find(text, "hurt itself in its confusion") or string.find(text, "must recharge") or string.find(text, "flinched") or string.find(text, "Come back") then is_action = true end if is_action then if not turn_header_printed then log_game_text("\nTurn " .. current_turn) turn_header_printed = true end actions_this_turn = actions_this_turn + 1 end end -- 7. MAIN FRAME LOOP function onFrame() local status, err = pcall(function() frames_since_write = frames_since_write + 1 if battle_trigger_timer > 0 then battle_trigger_timer = battle_trigger_timer - 1 end update_hp_tracker() local raw_text = decode_string(0x02022E2C, 80) if hp_snapshot_timer > 0 then if string.find(raw_text, "Come back") or string.find(raw_text, "Player defeated") or string.find(raw_text, "Got away") then hp_snapshot_timer = 0 tracked_hp = -1 end end if raw_text == potential_text then stable_text_counter = stable_text_counter + 1 else potential_text = raw_text stable_text_counter = 0 end if stable_text_counter == 2 then local current_text = raw_text if is_valid_text(current_text) then if current_text ~= last_text_hash then parse_enemy_arrival(current_text) parse_move_usage(current_text) if battle_over_lock then check_turn_logic(current_text) else check_turn_logic(current_text) if should_log_line(current_text) then log_game_text(current_text) local is_wipeout = false if string.find(current_text, "fainted") then is_wipeout = check_player_wipeout() end if string.find(current_text, "Player defeated") or string.find(current_text, "Got away") or string.find(current_text, "whited out") or string.find(current_text, "lost against") or string.find(current_text, "forfeited") or is_wipeout then update_hp_tracker() save_intel_to_log("--- FINAL BATTLE REPORT ---") battle_over_lock = true tracked_hp = -1 end end end last_text_hash = current_text end end end end) if not status then console:log("SCRIPT ERROR: " .. tostring(err)) end end console:log("Logger Started. Saving to: " .. LOG_FILE) scan_party() callbacks:add("frame", onFrame)