Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3bb8af7
Add config that skips font optimizations
flagrama Feb 9, 2025
04f5dff
Create more specific path funcs
flagrama Feb 9, 2025
a857599
GUI: Use XDG_CONFIG_HOME if needed on Linux
flagrama Feb 9, 2025
27b3085
Rename used path functions to indicate reading only
flagrama Feb 10, 2025
7def865
Use user dir functions
flagrama Feb 10, 2025
9369111
Use makedirs instead of mkdir
flagrama Feb 10, 2025
a4d0d6d
Fix paths
flagrama Feb 10, 2025
9feeb04
XDG path for ARCHIVE.bin
flagrama Feb 10, 2025
a4edda4
Open user-writable directories with GUI buttons
flagrama Feb 11, 2025
8289419
Move XDG path code, alert and quit app on write fail
flagrama Feb 11, 2025
828021d
Crosscompiled AARCH64 Compress
flagrama Feb 11, 2025
3950639
Create Music if doesn't exist
flagrama Feb 11, 2025
9eae2f6
Refactor user paths code
flagrama Feb 11, 2025
fa40a49
Copy music exclusion if it is in read-only directory
flagrama Feb 11, 2025
1c2f99f
Merge branch 'Dev' into flatpak-prep
flagrama Feb 11, 2025
4d3c707
format fixing
flagrama Feb 11, 2025
497e76f
handle common key and WAD injecting
flagrama Feb 12, 2025
46ded5e
util: find_user_path on DATA should return 'data' if writable
flagrama Feb 28, 2025
24c8043
cosmetics: break after finding voice pack
flagrama Feb 28, 2025
c879e2c
util: Fix default output path
flagrama Feb 28, 2025
3611218
Update Utils.py
flagrama May 17, 2025
968c37b
Merge branch 'Dev' into flatpak-prep
flagrama May 17, 2025
cf3d6ce
post-merge fix
flagrama May 17, 2025
e372798
Merge branch 'Dev' into flatpak-prep
flagrama Sep 29, 2025
b7d0197
Use alternative directories for presets if necessary
flagrama Sep 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions CI.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from Messages import ITEM_MESSAGES, IMPORTANT_ITEM_MESSAGES, MISC_MESSAGES
from SettingsList import SettingInfos, logic_tricks, validate_settings
import Unittest as Tests
from Utils import data_path
from Utils import readonly_data_path


def error(msg: str, can_fix: bool) -> None:
Expand Down Expand Up @@ -47,7 +47,7 @@ def run_unit_tests() -> None:

def check_presets_formatting(fix_errors: bool = False) -> None:
# Check the code style of presets_default.json
with open(data_path('presets_default.json'), encoding='utf-8') as f:
with open(readonly_data_path('presets_default.json'), encoding='utf-8') as f:
presets = json.load(f)

any_errors = False
Expand All @@ -63,13 +63,13 @@ def check_presets_formatting(fix_errors: bool = False) -> None:
if any_errors:
return

with open(data_path('presets_default.json'), encoding='utf-8') as f:
with open(readonly_data_path('presets_default.json'), encoding='utf-8') as f:
presets_str = f.read()

if presets_str != json.dumps(presets, indent=4) + '\n':
error('presets not formatted correctly', True)
if fix_errors:
with open(data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
with open(readonly_data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
json.dump(presets, file, indent=4)
print(file=file)
else:
Expand All @@ -88,15 +88,15 @@ def check_presets_formatting(fix_errors: bool = False) -> None:
if presets_str != json.dumps(presets, indent=4) + '\n':
error('presets not sorted correctly', True)
if fix_errors:
with open(data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
with open(readonly_data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
json.dump(presets, file, indent=4)
print(file=file)


def check_hell_mode_tricks(fix_errors: bool = False) -> None:
# Check for tricks missing from Hell Mode preset.
# Not checking for advanced logic tricks since hellmode is still glitchless logic
with open(data_path('presets_default.json'), encoding='utf-8') as f:
with open(readonly_data_path('presets_default.json'), encoding='utf-8') as f:
presets = json.load(f)

for trick in logic_tricks.values():
Expand All @@ -109,14 +109,14 @@ def check_hell_mode_tricks(fix_errors: bool = False) -> None:

if fix_errors:
presets['Hell Mode']['allowed_tricks'] = [trick['name'] for trick in logic_tricks.values()]
with open(data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
with open(readonly_data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
json.dump(presets, file, indent=4)
print(file=file)


def check_preset_spoilers(fix_errors: bool = False) -> None:
# Check to make sure spoiler logs are enabled for all presets.
with open(data_path('presets_default.json'), encoding='utf-8') as f:
with open(readonly_data_path('presets_default.json'), encoding='utf-8') as f:
presets = json.load(f)

for preset_name, preset in presets.items():
Expand All @@ -125,7 +125,7 @@ def check_preset_spoilers(fix_errors: bool = False) -> None:
preset['create_spoiler'] = True

if fix_errors:
with open(data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
with open(readonly_data_path('presets_default.json'), 'w', encoding='utf-8', newline='') as file:
json.dump(presets, file, indent=4)
print(file=file)

Expand Down Expand Up @@ -224,7 +224,7 @@ def check_table_sizes() -> None:
resolve_settings(settings)
world = World(0,settings)
for filename in ('Overworld.json', 'Bosses.json'):
world.load_regions_from_json(os.path.join(data_path('World'), filename))
world.load_regions_from_json(os.path.join(readonly_data_path('World'), filename))
world.create_dungeons()

xflags_tables, alt_list = build_xflags_from_world(world)
Expand Down
32 changes: 19 additions & 13 deletions Cosmetics.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import Sounds
from JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict
from Plandomizer import InvalidFileException
from Utils import data_path
from Utils import readonly_data_path, user_data_path
from version import __version__

if TYPE_CHECKING:
Expand Down Expand Up @@ -637,8 +637,8 @@ def patch_magic_colors(rom: Rom, settings: Settings, log: CosmeticsLog, symbols:
rom.write_int16s(symbol, color)
if magic_option != 'Green' and settings.correct_model_colors:
patch_model_colors(rom, color, model_addresses)
IconManip.patch_overworld_icon(rom, color, 0xF45650, data_path('icons/magicSmallExtras.raw')) # Overworld Small Pot
IconManip.patch_overworld_icon(rom, color, 0xF47650, data_path('icons/magicLargeExtras.raw')) # Overworld Big Pot
IconManip.patch_overworld_icon(rom, color, 0xF45650, readonly_data_path('icons/magicSmallExtras.raw')) # Overworld Small Pot
IconManip.patch_overworld_icon(rom, color, 0xF47650, readonly_data_path('icons/magicLargeExtras.raw')) # Overworld Big Pot
else:
patch_model_colors(rom, None, model_addresses)
IconManip.patch_overworld_icon(rom, None, 0xF45650)
Expand Down Expand Up @@ -852,7 +852,7 @@ def read_default_voice_data(rom: Rom) -> dict[str, dict[str, int]]:


def patch_silent_voice(rom: Rom, sfxidlist: Iterable[int], soundbank_entries: dict[str, dict[str, int]], log: CosmeticsLog) -> None:
binsfxfilename = os.path.join(data_path('Voices'), 'SilentVoiceSFX.bin')
binsfxfilename = os.path.join(readonly_data_path('Voices'), 'SilentVoiceSFX.bin')
if not os.path.isfile(binsfxfilename):
log.errors.append(f"Could not find silent voice sfx at {binsfxfilename}. Skipping voice patching")
return
Expand Down Expand Up @@ -913,16 +913,22 @@ def patch_voices(rom: Rom, settings: Settings, log: CosmeticsLog, symbols: dict[
if voice_setting == 'Silent':
patch_silent_voice(rom, silence_sfx_ids, soundbank_entries, log)
elif voice_setting != 'Default':
age_path = os.path.join(data_path('Voices'), name)
voice_path = os.path.join(age_path, voice_setting) if os.path.isdir(os.path.join(age_path, voice_setting)) else None

# If we don't have a confirmed directory for this voice, do a case-insensitive directory search.
if voice_path is None:
voice_dirs = [f for f in os.listdir(age_path) if os.path.isdir(os.path.join(age_path, f))] if os.path.isdir(age_path) else []
for directory in voice_dirs:
if directory.casefold() == voice_setting.casefold():
voice_path = os.path.join(age_path, directory)
randomizer_age_path = readonly_data_path(os.path.join('Voices', name))
user_age_path = user_data_path(os.path.join('Voices', name))
for age_path in [randomizer_age_path, user_age_path]:
voice_path = os.path.join(age_path, voice_setting) if os.path.isdir(os.path.join(age_path, voice_setting)) else None

# If we don't have a confirmed directory for this voice, do a case-insensitive directory search.
if voice_path is None:
voice_dirs = [f for f in os.listdir(age_path) if os.path.isdir(os.path.join(age_path, f))] if os.path.isdir(age_path) else []
for directory in voice_dirs:
if directory.casefold() == voice_setting.casefold():
voice_path = os.path.join(age_path, directory)
break
if voice_path is not None:
break
else:
break

if voice_path is None:
log.errors.append(f"{name} Voice not patched: Cannot find voice data directory: {os.path.join(age_path, voice_setting)}")
Expand Down
7 changes: 7 additions & 0 deletions GUI/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@
}
]
},
"flatpak": {
"optimization": {
"scripts": true,
"styles": true,
"fonts": false
}
},
"hmr": {
"budgets": [
{
Expand Down
6 changes: 4 additions & 2 deletions GUI/electron/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ function manageCSP() {
ipcMain.on('getGeneratorGUISettings', (event, arg) => {

let pythonRootPath = app.isPackaged ? app.getAppPath() + "/python/" : app.getAppPath() + "/../";
let userConfigPath = arg.configPath;
let userDataPath = arg.dataPath;

//Load compiled settings_list.json
let compiledSettingsMapPath = path.normalize(pythonRootPath + "data/generated/settings_list.json");
Expand All @@ -350,7 +352,7 @@ ipcMain.on('getGeneratorGUISettings', (event, arg) => {

//Load built in presets
let presetPaths: string[] = [path.normalize(pythonRootPath + "data/presets_default.json")];
let extraPresetsPath = path.normalize(pythonRootPath + "data/Presets");
let extraPresetsPath = path.normalize(userDataPath + "Presets");
let adjustedBuiltInPresets = {};

if (fs.existsSync(extraPresetsPath)) {
Expand All @@ -377,7 +379,7 @@ ipcMain.on('getGeneratorGUISettings', (event, arg) => {


//Load user presets
let userPresetPath = path.normalize(pythonRootPath + "presets.sav");
let userPresetPath = path.normalize(userConfigPath + "presets.sav");

if (fs.existsSync(userPresetPath)) {
let userPresets = JSON.parse(fs.readFileSync(userPresetPath, 'utf8'));
Expand Down
49 changes: 40 additions & 9 deletions GUI/electron/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ var pythonSettingsToJsonPath = pythonSourcePath + "SettingsToJson.py";
console.log("Python Executable Path:", pythonPath);
console.log("Python Source Path:", pythonGeneratorPath);

var localConfigWritePath: string;
var localDataWritePath: string;

//Enable API in client window
electron.webFrame.executeJavaScript('window.electronAvailable = true;');
electron.webFrame.executeJavaScript('window.apiTestMode = ' + testMode + ';');
Expand Down Expand Up @@ -59,11 +62,12 @@ remote.getCurrentWindow().on('leave-html-full-screen', () => {
//FUNCTIONS
function dumpSettingsToFile(settingsObj) {
settingsObj["check_version"] = true;
fs.writeFileSync(pythonSourcePath + "settings.sav", JSON.stringify(settingsObj, null, 4));

fs.writeFileSync(path.join(localConfigWritePath, "settings.sav"), JSON.stringify(settingsObj, null, 4));
}

function dumpPresetsToFile(presetsString: string) {
fs.writeFileSync(pythonSourcePath + "presets.sav", presetsString);
fs.writeFileSync(path.join(localConfigWritePath, "presets.sav"), presetsString);
}

function displayPythonErrorAndExit(notPython3: boolean = false) {
Expand All @@ -81,10 +85,10 @@ function displayPythonErrorAndExit(notPython3: boolean = false) {

function readSettingsFromFile() {

let path = pythonSourcePath + "settings.sav";
let settings = path.join(localConfigWritePath, "settings.sav");

if (fs.existsSync(path))
return fs.readFileSync(path, 'utf8');
if (fs.existsSync(settings))
return fs.readFileSync(settings, 'utf8');
else
return false;
}
Expand Down Expand Up @@ -126,7 +130,7 @@ post.on('getCurrentSourceVersion', function (event) {

post.on('getGeneratorGUISettings', function (event) {

return electron.ipcRenderer.sendSync('getGeneratorGUISettings');
return electron.ipcRenderer.sendSync('getGeneratorGUISettings', {"configPath":localConfigWritePath, "dataPath":localDataWritePath});
});

post.on('getGeneratorGUILastUserSettings', function (event) {
Expand Down Expand Up @@ -167,11 +171,11 @@ post.on('createAndOpenPath', function (event) {

//Use python dir if not specified otherwise
if (!data || typeof (data) != "string" || data.length < 1) {
data = pythonSourcePath;
data = localDataWritePath;
}
else {
if (!path.isAbsolute(data))
data = pythonSourcePath + data;
data = path.join(localDataWritePath, data);
}

if (fs.existsSync(data)) {
Expand Down Expand Up @@ -384,4 +388,31 @@ generator.testPythonPath(pythonPath).then(() => {
}).catch(err => {
console.error(err);
displayPythonErrorAndExit();
});
});

// Test if source path is writable. If not, use user-specified locations, or if not specified sane defaults
try {
fs.accessSync(pythonSourcePath, fs.constants.W_OK)
localConfigWritePath = localDataWritePath = pythonSourcePath;
} catch (err) {
// Cannot write to application directory. Either sandboxed or installed for all users.
if (platform === 'linux') {
const xdgConfig = path.join(fs.existsSync(process.env.XDG_CONFIG_HOME) ? process.env.XDG_CONFIG_HOME : path.join(os.homedir(), '.config'), 'ootr-electron-gui');
const xdgData = path.join(fs.existsSync(process.env.XDG_DATA_HOME) ? process.env.XDG_DATA_HOME : path.join(os.homedir(), '.local', 'share'), 'ootr-electron-gui');
[xdgConfig, xdgData].forEach((xdgPath: string) => {
if (!fs.existsSync(xdgPath)) {
fs.mkdirSync(xdgPath, {recursive: true, mode: 0o700});
}
fs.accessSync(xdgPath, fs.constants.W_OK);
});

localConfigWritePath = path.normalize(xdgConfig + '/');
localDataWritePath = path.normalize(xdgData + '/');
} else {
alert("Read-only paths are not supported on this platform.");
remote.app.quit();
}
}

console.log("Configuration Write Path:", localConfigWritePath);
console.log("Data Write Path:", localDataWritePath);
Loading