local SaveData = require("src.core.SaveData") local SaveIO = {} local function join(a, b) if a:sub(-1) == "/" or a:sub(-1) == "\\" then return a .. b end local sep = package.config:sub(1, 1) return a .. sep .. b end local function trim(value) return value and value:gsub("^%s+", ""):gsub("%s+$", "") or "" end local function commandOutput(command) local pipe = io.popen(command, "r") if not pipe then return nil end local result = pipe:read("*a") pipe:close() result = trim(result) return result ~= "" and result or nil end function SaveIO.defaultPath() -- same override conf.lua honors, so a mod-dev profile edits the save it -- actually plays local identity = os.getenv("POKEPORT_IDENTITY") or "pokemon-love2d" local home = os.getenv("HOME") or os.getenv("USERPROFILE") or "" local uname = io.popen and io.popen("uname -s 2>/dev/null") local sys = uname and uname:read("*l") or "" if uname then uname:close() end if sys == "Darwin" then -- LÖVE identity folder lives under Application Support/LOVE/ return join(join(join(join(home, "Library"), "Application Support"), "LOVE"), join(identity, "save.lua")) end -- Linux LOVE default if home ~= "" and sys ~= "" then return join(join(join(home, ".local/share"), "love"), join(identity, "save.lua")) end -- Windows local appdata = os.getenv("APPDATA") if appdata then return join(join(appdata, "love"), join(identity, "save.lua")) end return join(identity, "save.lua") end -- Native file picker (same approach as RomImporter). Returns an absolute -- path, or nil if the user cancelled / no dialog is available. function SaveIO.choosePath() local platform = (love and love.system and love.system.getOS and love.system.getOS()) or "" if platform == "OS X" then return commandOutput( [[osascript -e 'POSIX path of (choose file with prompt "Choose a Pokemon save.lua")' 2>/dev/null]]) elseif platform == "Windows" then local script = table.concat({ "Add-Type -AssemblyName System.Windows.Forms;", "$d=New-Object System.Windows.Forms.OpenFileDialog;", "$d.Title='Choose a Pokemon save.lua';", "$d.Filter='Save files (*.lua)|*.lua|All files (*.*)|*.*';", "if($d.ShowDialog() -eq 'OK'){[Console]::Write($d.FileName)}", }) return commandOutput( 'powershell -NoProfile -STA -Command "' .. script .. '"') elseif platform == "Linux" then local path = commandOutput( [[zenity --file-selection --title="Choose a Pokemon save.lua" --file-filter="Lua save | *.lua" 2>/dev/null]]) if path then return path end return commandOutput( [[kdialog --getopenfilename "$HOME" "*.lua|Lua save" 2>/dev/null]]) end return nil end function SaveIO.backupPath(path) local stamp = os.date("%Y%m%d-%H%M%S") return path .. ".bak-" .. stamp end function SaveIO.load(path) local f, err = io.open(path, "rb") if not f then return nil, err end local body = f:read("*a") f:close() return SaveData.decode(body) end function SaveIO.save(path, data) local encoded = SaveData.encode(data) local existing = io.open(path, "rb") if existing then local prev = existing:read("*a") existing:close() local bak = SaveIO.backupPath(path) local bf, berr = io.open(bak, "wb") if not bf then return false, berr end local bwok, bwerr = bf:write(prev) if not bwok then bf:close() os.remove(bak) return false, bwerr end local bcok, bcerr = bf:close() if not bcok then os.remove(bak) return false, bcerr end end local tmp = path .. ".tmp" local f, err = io.open(tmp, "wb") if not f then return false, err end local wok, werr = f:write(encoded) if not wok then f:close() os.remove(tmp) return false, werr end local cok, cerr = f:close() if not cok then os.remove(tmp) return false, cerr end local ok, rerr = os.rename(tmp, path) if not ok then -- Windows may need remove-first; try best-effort os.remove(path) ok, rerr = os.rename(tmp, path) end if not ok then return false, tostring(rerr) end return true end return SaveIO