Last active
November 24, 2025 23:41
-
-
Save eadmaster/f3c674f70544788f2f7f2ec6e133c986 to your computer and use it in GitHub Desktop.
RetroGuides.lua
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- RetroGuides - Online guide for retro games. | |
| -- Copyright (C) 2025 - eadmaster | |
| -- https://github.com/eadmaster/eadmaster/ | |
| -- | |
| -- RetroGuides is free software: you can redistribute it and/or modify it under the terms | |
| -- of the GNU General Public License as published by the Free Software Found- | |
| -- ation, either version 3 of the License, or (at your option) any later version. | |
| -- | |
| -- RetroGuides is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; | |
| -- without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR | |
| -- PURPOSE. See the GNU General Public License for more details. | |
| -- | |
| -- You should have received a copy of the GNU General Public License along with RetroSubs. | |
| -- If not, see <http://www.gnu.org/licenses/>. | |
| -- Shows a guide ingame from a txt file or a markdown with multiple sections. | |
| -- Controls: P1 Start+Select=Toggle guide ; d-pad up/down = scroll up/down | |
| -- The txt file should match the content filename to be found. | |
| -- e.g. "Bomberman (USA).nes" -> "Bomberman (USA).txt" | |
| local global_guide_filename = "../vg.md" -- must be a markdown file | |
| local using_global_guide_file = true | |
| local file = io.open(global_guide_filename, "r") | |
| if not file then | |
| print("global guide file not found: " .. global_guide_filename) | |
| using_global_guide_file = false | |
| file = io.open(gameinfo.getromname() .. ".txt") | |
| if file == nil then | |
| -- try to load from content dir | |
| local curr_rom_path = nil | |
| if gameinfo and type(gameinfo.getrompath) == "function" then | |
| curr_rom_path = gameinfo.getrompath() -- retroarch-only | |
| end | |
| local config = client.getconfig() -- bizhawk-only | |
| if config.RecentRoms and config.RecentRoms[0] then | |
| curr_rom_path = config.RecentRoms[0] | |
| curr_rom_path = curr_rom_path:gsub("%*OpenRom%*", "") | |
| end | |
| if curr_rom_path then | |
| curr_rom_path = curr_rom_path:gsub("%.[^%.]+$", ".txt") -- Replace the extension | |
| file = io.open(curr_rom_path) | |
| end | |
| if file == nil then | |
| print("file not found: " .. curr_rom_path) | |
| -- TODO: if nothing found, show a menu to select a txt file to open | |
| return | |
| end | |
| end | |
| end | |
| local lines = {} | |
| for line in file:lines() do | |
| table.insert(lines, line) | |
| end | |
| file:close() | |
| local found = false | |
| local result = {} | |
| -- Remove tags like "(...)" or "[...]" and trim spaces | |
| local function clean_header(name) | |
| local cleaned = name:gsub("%b()", ""):gsub("%b[]", "") | |
| cleaned = cleaned:gsub("%s+", " "):gsub("^%s*(.-)%s*$", "%1") -- trim | |
| return cleaned | |
| end | |
| local function lower(s) | |
| return string.lower(s) | |
| end | |
| -- Helper: word-wrap a long line into smaller lines | |
| function wrap_line(line) | |
| local MAX_LINE_LENGTH = 45 -- max columns before wrapping | |
| local wrapped = {} | |
| while #line > MAX_LINE_LENGTH do | |
| local chunk = line:sub(1, MAX_LINE_LENGTH) | |
| local last_space = chunk:match(".*()[%s%-_/.,;:!?]") -- find last word boundary | |
| if last_space and last_space > 20 then | |
| table.insert(wrapped, line:sub(1, last_space)) | |
| line = line:sub(last_space + 1):gsub("^%s*", "") | |
| else | |
| table.insert(wrapped, chunk) | |
| line = line:sub(MAX_LINE_LENGTH + 1) | |
| end | |
| end | |
| table.insert(wrapped, line) | |
| return wrapped | |
| end | |
| starting_line = 0 -- global to keep state after closing | |
| function show_text(text_lines) | |
| local x_pos = 5 -- default: upper-left corner | |
| local y_pos = 5 -- default: upper-left corner | |
| local bg_color = "#DF000000" --0xEF000000 -- default: black (0xAARRGGBB) | |
| local fg_color = 0xFFFFFFFF -- default: white | |
| local height_box = 400 -- optional | |
| local width_box = 400 -- optional | |
| local font_size = 8 -- optional -- client.getconfig().FontSize | |
| --local font_face = "Arial" -- optional | |
| local TEXTBOX_PADDING = 2 | |
| local LINE_SPACING = font_size | |
| -- different defaults for retroarch | |
| --if CURRENT_EMU == "retroarch" then | |
| --font_size = 32 + (font_size - 12) -- * scaling_factor | |
| --font_face = "" | |
| --end | |
| local needs_redraw = true | |
| while true do | |
| if needs_redraw then | |
| needs_redraw = false | |
| gui.drawRectangle(x_pos, y_pos, width_box, height_box, bg_color, bg_color) | |
| -- Split text into lines using <br> | |
| local line_count = 0 | |
| local curr_y_pos = y_pos + line_count * LINE_SPACING | |
| for i, raw_line in ipairs(text_lines) do | |
| if i >= starting_line then | |
| -- gui.drawString(int x, int y, string message, [luacolor forecolor = nil], [luacolor backcolor = nil], [int? fontsize = nil], [string fontfamily = nil], [string fontstyle = nil], [string horizalign = nil], [string vertalign = nil], [string surfacename = nil]) | |
| for _, line in ipairs(wrap_line(raw_line)) do | |
| curr_y_pos = y_pos + line_count * LINE_SPACING | |
| gui.pixelText(x_pos + TEXTBOX_PADDING, curr_y_pos, line) --, fg_color , nil, font_size, font_face) | |
| line_count = line_count + 1 | |
| end | |
| --emu.pause() | |
| --print(curr_y_pos) | |
| --client.sleep(200) | |
| --if line_count > 30 then | |
| -- break | |
| --end | |
| if(curr_y_pos > client.bufferheight()) then | |
| break | |
| end | |
| end | |
| end | |
| end | |
| --emu.frameadvance(); | |
| --client.sleep(500) -- debounce | |
| --emu.pause() | |
| emu.frameadvance(); | |
| --local pressed_keys = input.get() | |
| --if pressed_keys[GUIDE_HOTKEY] then | |
| if emu.framecount() % 10 == 0 then -- debounce | |
| local joy = joypad.get() | |
| if(joy["P1 Start"]=="True" and joy["P1 Select"]=="True") then | |
| gui.cleartext() | |
| gui.clearGraphics() | |
| --emu.unpause() | |
| return | |
| elseif (joy["P1 Down"]=="True") then | |
| gui.cleartext() | |
| gui.clearGraphics() | |
| starting_line = starting_line + 1 | |
| needs_redraw = true | |
| elseif (joy["P1 Up"]=="True") then | |
| gui.cleartext() | |
| gui.clearGraphics() | |
| starting_line = starting_line - 1 | |
| if(starting_line)<0 then | |
| starting_line = 0 | |
| end | |
| needs_redraw = true | |
| end | |
| end | |
| end -- while | |
| end | |
| function find_text(target_header) | |
| for i, line in ipairs(lines) do | |
| local header = line:match("^##%s*(.+)") | |
| if header then | |
| if found then | |
| break -- reached next header, stop collecting | |
| end | |
| -- substring, case-insensitive match | |
| if header:find(target_header, 1, true) then | |
| found = true | |
| end | |
| elseif found then | |
| --if line:lower():match("^sources:") then | |
| -- break | |
| -- end | |
| table.insert(result, line) | |
| end | |
| end | |
| if #result == 0 then | |
| print("Header not found or section is empty.") | |
| return false | |
| else | |
| print(table.concat(result, "\n")) | |
| show_text(result) | |
| --gui.addmessage("guide found, press Select+Start to see it") | |
| return true | |
| end | |
| end | |
| if using_global_guide_file then | |
| -- look and extract the subsection | |
| local target_header = clean_header(gameinfo.getromname()) | |
| if not find_text(target_header) then | |
| --gui.addmessage("guide not found: " .. target_header) | |
| local curr_rom_path = "" | |
| if gameinfo and type(gameinfo.getrompath) == "function" then | |
| curr_rom_path = gameinfo.getrompath() -- retroarch-only | |
| end | |
| local config = client.getconfig() -- bizhawk-only | |
| if config.RecentRoms and config.RecentRoms[0] then | |
| curr_rom_path = config.RecentRoms[0] | |
| curr_rom_path = curr_rom_path:gsub("%*OpenRom%*", "") | |
| end | |
| if curr_rom_path then | |
| curr_rom_path = curr_rom_path:gsub("%.[^%.]+$", "") -- Replace the extension | |
| target_header = clean_header(curr_rom_path) | |
| end | |
| if not find_text(target_header) then | |
| print("guide not found: " .. target_header) | |
| gui.addmessage("guide not found: " .. target_header) | |
| return | |
| end | |
| end | |
| else | |
| -- just insert all the lines | |
| for _, line in ipairs(lines) do | |
| table.insert(result, line) | |
| end | |
| show_text(result) | |
| --gui.addmessage("guide found, press Select+Start to see it") | |
| end | |
| -- main loop | |
| while true do | |
| if emu.framecount() % 10 == 0 then -- debounce | |
| local joy = joypad.get() | |
| if(joy["P1 Start"]=="True" and joy["P1 Select"]=="True") then | |
| --print("guide") | |
| show_text(result) | |
| end | |
| end | |
| emu.frameadvance(); | |
| end -- while | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment