IO: Resolve IO DLL paths from environment variables (#119)

## Summary

Adds a unified mechanism for locating third-party IO DLLs through environment
variables, on top of the existing per-game `[xxxio] path=` INI setting.

## Changes

For each IO DLL, the path is resolved in this order:

1. The section-specific INI value, e.g. `[mai2io] path=`.
2. The section-specific environment variable, e.g. `SEGATOOLS_MAI2IO_PATH`.
3. `SEGATOOLS_IO_ROOT` joined with the default IO DLL filename, e.g.
   `%SEGATOOLS_IO_ROOT%\mai2io.dll`.
4. Empty path, which keeps the built-in IO implementation (unchanged default).

INI and environment-variable paths are run through Windows environment-variable
expansion, so values such as `%SEGATOOLS_IO_ROOT%\mai2io.dll` are valid in the
INI directly.

Reviewed-on: https://gitea.tendokyu.moe/TeamTofuShop/segatools/pulls/119
Co-authored-by: グローランプ <130208311+Gl0w1amp@users.noreply.github.com>
Co-committed-by: グローランプ <130208311+Gl0w1amp@users.noreply.github.com>
This commit is contained in:
グローランプ
2026-07-08 20:23:08 +00:00
committed by Dniel97
parent 8da836ccfb
commit 89bd77e306
44 changed files with 501 additions and 103 deletions
+14 -11
View File
@@ -11,6 +11,7 @@
#include "board/vfd.h"
#include "util/dprintf.h"
#include "util/io-path.h"
// Check windows
#if _WIN32 || _WIN64
@@ -44,24 +45,26 @@ static void aime_dll_config_load(struct aime_dll_config *cfg, const wchar_t *fil
// Always empty, due to amdaemon being 64 bit in 32 bit mode
memset(cfg->path, 0, sizeof(cfg->path));
#elif defined(ENV64BIT)
GetPrivateProfileStringW(
L"aimeio",
L"path",
L"",
io_path_config_load(
cfg->path,
_countof(cfg->path),
L"aimeio",
L"path",
L"SEGATOOLS_AIMEIO_PATH",
L"aimeio.dll",
filename);
#else
#error "Unknown environment"
#endif
} else {
GetPrivateProfileStringW(
L"aimeio",
L"path",
L"",
cfg->path,
_countof(cfg->path),
filename);
io_path_config_load(
cfg->path,
_countof(cfg->path),
L"aimeio",
L"path",
L"SEGATOOLS_AIMEIO_PATH",
L"aimeio.dll",
filename);
}
}
+7 -5
View File
@@ -9,6 +9,7 @@
#include "hooklib/dvd.h"
#include "hooklib/y3.h"
#include "hooklib/y3-dll.h"
#include "util/io-path.h"
void dvd_config_load(struct dvd_config *cfg, const wchar_t *filename)
{
@@ -158,12 +159,13 @@ void y3_dll_config_load(
assert(cfg != NULL);
assert(filename != NULL);
GetPrivateProfileStringW(
L"y3io",
L"path",
L"",
io_path_config_load(
cfg->path,
_countof(cfg->path),
L"y3io",
L"path",
L"SEGATOOLS_Y3IO_PATH",
L"y3io.dll",
filename);
}
@@ -243,4 +245,4 @@ void y3_config_load(
filename);
wcstombs(cfg->target_code_printer, tmpstr, sizeof(cfg->target_code_printer) - 1);
}
}
+203
View File
@@ -0,0 +1,203 @@
#include <windows.h>
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include "util/dprintf.h"
#include "util/io-path.h"
static bool io_path_expand(
wchar_t *out,
size_t out_count,
const wchar_t *path,
const wchar_t *source);
static bool io_path_from_root(
wchar_t *out,
size_t out_count,
const wchar_t *root_filename);
static bool io_path_is_separator(wchar_t ch);
bool io_path_config_load(
wchar_t *out,
size_t out_count,
const wchar_t *section,
const wchar_t *key,
const wchar_t *env_name,
const wchar_t *root_filename,
const wchar_t *filename)
{
wchar_t path[MAX_PATH];
DWORD count;
assert(out != NULL);
assert(out_count > 0);
assert(section != NULL);
assert(key != NULL);
assert(filename != NULL);
out[0] = L'\0';
count = GetPrivateProfileStringW(
section,
key,
L"",
path,
_countof(path),
filename);
if (count >= _countof(path) - 1 && path[0] != L'\0') {
dprintf("IO path: INI value [%S] %S may be truncated "
"(exceeds %d characters)\n",
section,
key,
(int) _countof(path) - 1);
}
if (path[0] != L'\0') {
return io_path_expand(out, out_count, path, key);
}
if (env_name != NULL) {
count = GetEnvironmentVariableW(env_name, path, _countof(path));
if (count >= _countof(path)) {
dprintf("IO path: Environment variable too long: %S\n", env_name);
return false;
}
if (count != 0 && path[0] != L'\0') {
return io_path_expand(out, out_count, path, env_name);
}
}
return io_path_from_root(out, out_count, root_filename);
}
static bool io_path_expand(
wchar_t *out,
size_t out_count,
const wchar_t *path,
const wchar_t *source)
{
DWORD expanded;
assert(out != NULL);
assert(out_count > 0);
assert(path != NULL);
assert(source != NULL);
expanded = ExpandEnvironmentStringsW(path, out, (DWORD) out_count);
if (expanded == 0) {
dprintf("IO path: Failed to expand path from %S: %lx\n",
source,
GetLastError());
out[0] = L'\0';
return false;
}
if (expanded > out_count) {
dprintf("IO path: Expanded path from %S is too long: %S\n",
source,
path);
out[0] = L'\0';
return false;
}
return true;
}
static bool io_path_from_root(
wchar_t *out,
size_t out_count,
const wchar_t *root_filename)
{
wchar_t root[MAX_PATH];
wchar_t expanded_root[MAX_PATH];
size_t root_len;
size_t file_len;
DWORD count;
assert(out != NULL);
assert(out_count > 0);
if (root_filename == NULL || root_filename[0] == L'\0') {
return false;
}
count = GetEnvironmentVariableW(
L"SEGATOOLS_IO_ROOT",
root,
_countof(root));
if (count >= _countof(root)) {
dprintf("IO path: SEGATOOLS_IO_ROOT is too long\n");
return false;
}
if (count == 0 || root[0] == L'\0') {
return false;
}
if (!io_path_expand(
expanded_root,
_countof(expanded_root),
root,
L"SEGATOOLS_IO_ROOT")) {
return false;
}
root_len = wcslen(expanded_root);
file_len = wcslen(root_filename);
if (root_len == 0) {
return false;
}
if (root_len + file_len + 2 > out_count) {
dprintf("IO path: SEGATOOLS_IO_ROOT path is too long: %S\\%S\n",
expanded_root,
root_filename);
return false;
}
wcscpy_s(out, out_count, expanded_root);
if (!io_path_is_separator(out[root_len - 1])) {
out[root_len++] = L'\\';
out[root_len] = L'\0';
}
wcscat_s(out, out_count, root_filename);
/* The SEGATOOLS_IO_ROOT fallback is broadcast to every hook at once, so a
single global setting is shared by games that may not each ship an IO
DLL in that directory. Unlike an explicit INI/env path (direct user
intent, allowed to fail loudly at load time), a synthesized root path
that does not exist should quietly fall back to the built-in IO
implementation instead of aborting hook startup. */
if (GetFileAttributesW(out) == INVALID_FILE_ATTRIBUTES) {
dprintf("IO path: SEGATOOLS_IO_ROOT DLL not found, "
"using built-in IO: %S\n",
out);
out[0] = L'\0';
return false;
}
return true;
}
static bool io_path_is_separator(wchar_t ch)
{
return ch == L'\\' || ch == L'/';
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <windows.h>
#include <stdbool.h>
#include <stddef.h>
bool io_path_config_load(
wchar_t *out,
size_t out_count,
const wchar_t *section,
const wchar_t *key,
const wchar_t *env_name,
const wchar_t *root_filename,
const wchar_t *filename);
+2
View File
@@ -21,6 +21,8 @@ util_lib = static_library(
'fg-detect.h',
'get_function_ordinal.c',
'get_function_ordinal.h',
'io-path.c',
'io-path.h',
'lib.c',
'lib.h',
'slurp.c',