refactor: Cleanup develhook logic

> refactor hooks::devel to work better with hooks::wmisc
> minor naming consistency changes
This commit is contained in:
[ ]
2025-12-17 00:00:40 +09:00
parent 7f63b77ab3
commit 56e8e7456b
6 changed files with 253 additions and 205 deletions
+113 -98
View File
@@ -12,75 +12,84 @@
#include <sstream> #include <sstream>
namespace hooks::devel::thumbnail
{
typedef int (*BmswSymlink_t)(const char *srcwpath, const char *dstwpath, int canonic);
static BmswSymlink_t BmswSymlink;
void run()
{
HMODULE bmsw_ = nullptr;
if ((bmsw_ = libutils::try_library(MODULE_PATH / "bmsound-wine.dll")))
{
BmswSymlink = (BmswSymlink_t) GetProcAddress(bmsw_, "BmswSymlink");
}
log_warning("hooks::devel::thumbnail", "Forcing thumbnail patch..");
hooks::wmisc::apply_performance_hacks(avs::game::DLL_INSTANCE, "LDJ", "20250825");
}
}
namespace hooks::devel::avs_fs
{
static constexpr char needle_[] = ""; // set to "" to print all
static avs::core::AVS_FS_LSTAT_T avs_fs_lstat_;
static avs::core::AVS_FS_MOUNT_T avs_fs_mount_;
static avs::core::AVS_FS_OPEN_T avs_fs_open_;
int on_avs_fs_lstat(const char *path, struct avs::core::avs_stat *stat)
{
if (strstr(path, needle_))
{
log_info("hooks::devel", "{}({})", __FUNCTION__, path);
}
return avs_fs_lstat_(path, stat);
}
avs::core::avs_file_t on_avs_fs_mount(const char *mountpoint, const char *fsroot, const char *fstype, void *data)
{
if (strstr(fsroot, needle_))
{
log_info("hooks::devel", "{}({}=>{}) @{} :nullptr data?{} ", __FUNCTION__, fsroot, mountpoint, fstype, data == nullptr);
}
return avs_fs_mount_(mountpoint, fsroot, fstype, data);
}
avs::core::avs_file_t on_avs_fs_open(const char *name, uint16_t mode, int flags)
{
if (strstr(name, needle_))
{
log_info("hooks::devel", "{}({}::{}) @{}", __FUNCTION__, name, mode, flags);
}
return avs_fs_open_(name, mode, flags);
}
void run()
{
detour::trampoline(reinterpret_cast<void *>(avs::core::avs_fs_lstat), reinterpret_cast<void *>(on_avs_fs_lstat), reinterpret_cast<void **>(&avs_fs_lstat_));
detour::trampoline(reinterpret_cast<void *>(avs::core::avs_fs_mount), reinterpret_cast<void *>(on_avs_fs_mount), reinterpret_cast<void **>(&avs_fs_mount_));
detour::trampoline(reinterpret_cast<void *>(avs::core::avs_fs_open), reinterpret_cast<void *>(on_avs_fs_open), reinterpret_cast<void **>(&avs_fs_open_));
}
}
namespace hooks::devel namespace hooks::devel
{ {
void (*routine_cb_[devel_routine_last])() ={ static class Thumbnail : public Routine
[avs_fs_detour] = avs_fs::run, {
[sndb_test] = nullptr, public:
[thum_test] = thumbnail::run using Routine::Routine;
}; void entry_main() override
{
log_warning("hooks::devel::thumbnail", "Forcing thumbnail patch..");
hooks::wmisc::apply_performance_hacks(avs::game::DLL_INSTANCE, "FORCE", "thumbnail");
}
} thumbnail_("thum_test");
static class Msacm : public Routine
{
public:
using Routine::Routine;
void entry_main() override
{
log_warning("hooks::devel::msacm", "Forcing msacm patch..");
hooks::wmisc::apply_performance_hacks(avs::game::DLL_INSTANCE, "FORCE", "msacm");
}
} msacm_("acm_test");
static class Wmisc : public Routine
{
public:
using Routine::Routine;
void attach() override
{
log_warning("hooks::devel::wmisc", "Disabling auto-patching..");
hooks::wmisc::skip_patching_by_model(true);
}
} wmisc_("wmisc_test");
static class Avs_fs : public Routine
{
private:
static constexpr char needle_[] = ""; // set to "" to print all
inline static avs::core::AVS_FS_LSTAT_T avs_fs_lstat_;
inline static avs::core::AVS_FS_MOUNT_T avs_fs_mount_;
inline static avs::core::AVS_FS_OPEN_T avs_fs_open_;
static int on_avs_fs_lstat(const char *path, struct avs::core::avs_stat *stat)
{
if (strstr(path, needle_))
{
log_info("hooks::devel", "{}({})", __FUNCTION__, path);
}
return avs_fs_lstat_(path, stat);
}
static avs::core::avs_file_t on_avs_fs_mount(const char *mountpoint, const char *fsroot, const char *fstype, void *data)
{
if (strstr(fsroot, needle_))
{
log_info("hooks::devel", "{}({}=>{}) @{} :nullptr data?{} ", __FUNCTION__, fsroot, mountpoint, fstype, data == nullptr);
}
return avs_fs_mount_(mountpoint, fsroot, fstype, data);
}
static avs::core::avs_file_t on_avs_fs_open(const char *name, uint16_t mode, int flags)
{
if (strstr(name, needle_))
{
log_info("hooks::devel", "{}({}::{}) @{}", __FUNCTION__, name, mode, flags);
}
return avs_fs_open_(name, mode, flags);
}
public:
using Routine::Routine;
void entry_main() override
{
detour::trampoline(reinterpret_cast<void *>(avs::core::avs_fs_lstat), reinterpret_cast<void *>(on_avs_fs_lstat), reinterpret_cast<void **>(&avs_fs_lstat_));
detour::trampoline(reinterpret_cast<void *>(avs::core::avs_fs_mount), reinterpret_cast<void *>(on_avs_fs_mount), reinterpret_cast<void **>(&avs_fs_mount_));
detour::trampoline(reinterpret_cast<void *>(avs::core::avs_fs_open), reinterpret_cast<void *>(on_avs_fs_open), reinterpret_cast<void **>(&avs_fs_open_));
}
} avs_fs_("avs_fs_detour");
//static constexpr unsigned int to_hash(const char *str, int h = 0)
//{
// return !str[h] ? 5381 : (to_hash(str, h + 1) * 33) ^ str[h];
//}
std::vector<std::string> from_env(const char *str) std::vector<std::string> from_env(const char *str)
{ {
std::vector<std::string> r; std::vector<std::string> r;
@@ -94,50 +103,56 @@ std::vector<std::string> from_env(const char *str)
return r; return r;
} }
static devel_routine_t routines() static std::vector<Routine*> &routines()
{ {
static devel_routine_t routines_ = -1; static std::vector<Routine*> routines_;
if (routines_ == static_cast<devel_routine_t>(-1)) if (routines_.empty())
{ {
routines_ = 0; //Default routines
for (const auto &routine: from_env("SPICE_DEVEL")) routines_.push_back(&avs_fs_);
{ routines_.push_back(&thumbnail_);
switch (to_hash(routine.c_str())) routines_.push_back(&msacm_);
{ routines_.push_back(&wmisc_);
case to_hash("avs_fs_detour"):
routines_ |= 1 << avs_fs_detour;
break;
case to_hash("sndb_test"):
routines_ |= 1 << sndb_test;
break;
case to_hash("thum_test"):
routines_ |= 1 << thum_test;
break;
case to_hash("acm_test"):
routines_ |= 1 << acm_test;
break;
default:
break;
}
}
} }
return routines_; return routines_;
} }
bool active_routine(hooks::devel::devel_routine_t val) Routine *active_routine(const char *name)
{ {
return routines() & (1 << val); unsigned int hash = to_hash(name);
for (auto &routine: routines())
{
if (routine->hash() == hash)
return routine;
}
return nullptr;
} }
void bind_routine(devel_routine_t val, void (*cb)()) void bind_routine(Routine *routine)
{ {
routine_cb_[val] = cb; routines().push_back(routine);
}
void log_environment()
{
log_warning("hooks::devel", "WINEDLLOVERRIDES: '{}'", getenv("WINEDLLOVERRIDES") ? getenv("WINEDLLOVERRIDES") : "null");
} }
void entry_main() void entry_main()
{ {
if (routines()) log_info("hooks::devel", "Processing subroutines.."); log_environment();
for (devel_routine_t i = 0; i < devel_routine_last; i++) const auto rnames = from_env("SPICE_DEVEL");
if (!rnames.empty()) log_info("hooks::devel", "Run subroutines..");
for (const auto &routinename: from_env("SPICE_DEVEL"))
{ {
if (active_routine(i) && routine_cb_[i]) routine_cb_[i](); Routine *routine = active_routine(routinename.c_str());
if (routine) routine->entry_main();
}
}
void attach()
{
const auto rnames = from_env("SPICE_DEVEL");
if (!rnames.empty()) log_info("hooks::devel", "Attach subroutines..");
for (const auto &rname: rnames)
{
Routine *routine = active_routine(rname.c_str());
if (routine) routine->attach();
} }
} }
} }
+14 -9
View File
@@ -1,20 +1,25 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include "util/utils.h"
namespace hooks::devel namespace hooks::devel
{ {
//_INFO: order matters, low level routines first (if you want them active during later routines) //_INFO: use attach()/entry_main() to run task before game::attach()/game::entry_main()
enum devel_routine : uint32_t class Routine
{ {
avs_fs_detour = 0, private:
sndb_test, unsigned int hash_;
thum_test, public:
devel_routine_last [[nodiscard]] inline unsigned int constexpr hash() const { return hash_; }
virtual void attach() {}
virtual void entry_main() {}
explicit Routine(const char *name) : hash_(to_hash(name)) {}
virtual ~Routine() = default;
}; };
typedef uint32_t devel_routine_t;
bool active_routine(devel_routine_t val); Routine *active_routine(const char *name);
void bind_routine(devel_routine_t val, void (*cb)()); void bind_routine(Routine *routine);
void attach();
void entry_main(); void entry_main();
} }
+98 -93
View File
@@ -769,105 +769,110 @@ void *on_enum_valid_soundbanks(void *song, int32_t difficulty, int32_t unk2)
*((char *) song + offset_.DAT_song_id + 8 + difficulty) = suffix; *((char *) song + offset_.DAT_song_id + 8 + difficulty) = suffix;
return r; return r;
} }
void run_tests() static class Tests : public hooks::devel::Routine
{ {
size_t szs3p; public:
BYTE *wav = nullptr; using Routine::Routine;
uint32_t wavsz = 0; void entry_main() override
char avspath[AVSPATHMAX];
avs::core::avs_file_t avsmnt = 0;
char sig[5] = " ";
int id = 25073;
if (!mf_broken::init())
{ {
log_warning("hooks::soundbank", "MF initialization failed, skipping.."); size_t szs3p;
} BYTE *wav = nullptr;
uint32_t wavsz = 0;
// Filesystem tests, s3p unpacking char avspath[AVSPATHMAX];
snprintf(avspath, sizeof(avspath), "data/sound/%05d", id); avs::core::avs_file_t avsmnt = 0;
if (snd_bank::has_format(avspath, ".ifs")) avsmnt = snd_bank::map_ifs(avspath); char sig[5] = " ";
snprintf(avspath + strlen(avspath), 7, "/%05d", id); int id = 25073;
snd_bank::has_format(avspath, ".s3p"); if (!mf_broken::init())
snd_bank::has_format(avspath, ".2dx");
snd_bank::has_format(avspath, "_pre.2dx");
uint8_t *bufs3p = snd_bank::read_bank_any(avspath, ".s3p", &szs3p);
snd_s3p *snds3p = snd_s3p::unpack(bufs3p);
if (avsmnt) avs::core::avs_fs_umount(avsmnt);
// S3P => WMA => WAV => 2DX pipeline
if (snds3p)
{
// S3P/S3V metadata
memcpy(sig, snds3p->header()->sig, 4);
log_info("hooks::soundbank", "{}::Bank[S3P]::Signature {}", id, sig);
log_info("hooks::soundbank", "{}::Bank[S3P]::Size {}", id, szs3p);
log_info("hooks::soundbank", "{}::Bank[S3P]::VoiceCount {}", id, snds3p->header()->voice_cnt);
log_info("hooks::soundbank", "{}::Bank[S3P]::Voice[0]::Size {}", id, snds3p->voice_ptr(0)->sz);
memcpy(sig, snds3p->voice(0)->sig, 4);
log_info("hooks::soundbank", "{}::Bank[S3P]::Voice[0]::Signature {}", id, sig);
snd_bank::dump("raw_wma.wma", snds3p->voice(0)->asf(), snds3p->voice(0)->sz);
// MF codec (tainted PCM data, bad)
mf_broken::asf_to_wav(snds3p->voice(0)->asf(), snds3p->voice(0)->sz, &wav, &wavsz);
snd_bank::dump("mf_wav.wav", wav, wavsz);
free(wav);
wav = nullptr;
wavsz = 0;
// AV codec (1:1 unmodified PCM data, other than resampling if enabled)
wav = (BYTE *) malloc(snd_2dx::assertsz_asf(snds3p->voice(0)->sz, avcodec::bmswac_resampler_f32));
avcodec::BmswTranscoderAsfToWav(snds3p->voice(0)->asf(), snds3p->voice(0)->sz, &wav, &wavsz, avcodec::bmswac_resampler_f32);
snd_bank::dump("ac_wav.wav", wav, wavsz);
free(wav);
wav = nullptr;
wavsz = 0;
// S3P=>2DX (max 4076 voices)
size_t i, j;
snd_2dx *snd2dx = snd_2dx::pack(id);
log_info("hooks::soundbank", "{}::Bank[2DX]::Name {}", id, snd2dx->header()->name);
for (i = 0; i < snds3p->header()->voice_cnt; i++)
{ {
// WMA=>WAV transcode log_warning("hooks::soundbank", "MF initialization failed, skipping..");
snd_s3p::bank::voice_hdr *voicesrc = snds3p->voice(i);
j = snd2dx->voice_emplace(snd_2dx::assertsz_asf(voicesrc->sz));
snd_2dx::bank::voice_hdr *voicedst = snd2dx->voice(j);
BYTE *voicedst_wave = voicedst->wave();
//mf_broken::asf_to_wav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz);
avcodec::BmswTranscoderAsfToWav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz, snd_bank::resampler());
snd2dx->voice_ptr(j)->sz(voicedst); // updates voice_ptr table, do not skip
} }
// Valid 2DX (trim is not necessary) // Filesystem tests, s3p unpacking
log_info("hooks::soundbank", "{}::Bank[2DX]::Dumping", id); snprintf(avspath, sizeof(avspath), "data/sound/%05d", id);
snd_2dx::bank *buf2dx = snd2dx->serialize(false); if (snd_bank::has_format(avspath, ".ifs")) avsmnt = snd_bank::map_ifs(avspath);
snd_bank::dump("ac_soundbank.2dx", reinterpret_cast<const uint8_t *>(buf2dx), buf2dx->sz()); snprintf(avspath + strlen(avspath), 7, "/%05d", id);
} snd_bank::has_format(avspath, ".s3p");
else snd_bank::has_format(avspath, ".2dx");
{ snd_bank::has_format(avspath, "_pre.2dx");
log_info("hooks::soundbank", "Tests failed"); uint8_t *bufs3p = snd_bank::read_bank_any(avspath, ".s3p", &szs3p);
} snd_s3p *snds3p = snd_s3p::unpack(bufs3p);
if (avsmnt) avs::core::avs_fs_umount(avsmnt);
// Caching and intermediate 2dx storage detection // S3P => WMA => WAV => 2DX pipeline
BmsbEnumValidSoundbanks_t t = BmsbEnumValidSoundbanks; if (snds3p)
BmsbEnumValidSoundbanks = [](void *, int32_t, int32_t) -> void * { return nullptr; }; {
char *tmp = (char *) malloc(0x3b8 + strlen("00000000") + 1); // S3P/S3V metadata
int arr[6] = {25073, 24016, 26057, 25073, 26087, 29095}; memcpy(sig, snds3p->header()->sig, 4);
strcpy(tmp, "SJISTITLE"); log_info("hooks::soundbank", "{}::Bank[S3P]::Signature {}", id, sig);
strcpy(tmp + 0xc0, "SJISARTIST"); log_info("hooks::soundbank", "{}::Bank[S3P]::Size {}", id, szs3p);
strcpy(tmp + 0x3b8, "00000000"); log_info("hooks::soundbank", "{}::Bank[S3P]::VoiceCount {}", id, snds3p->header()->voice_cnt);
for (int it: arr) log_info("hooks::soundbank", "{}::Bank[S3P]::Voice[0]::Size {}", id, snds3p->voice_ptr(0)->sz);
{ memcpy(sig, snds3p->voice(0)->sig, 4);
*(int32_t *) (tmp + 0x3b0) = it; log_info("hooks::soundbank", "{}::Bank[S3P]::Voice[0]::Signature {}", id, sig);
on_enum_valid_soundbanks(tmp, 3, 0); snd_bank::dump("raw_wma.wma", snds3p->voice(0)->asf(), snds3p->voice(0)->sz);
}
free(tmp); // MF codec (tainted PCM data, bad)
BmsbEnumValidSoundbanks = t; mf_broken::asf_to_wav(snds3p->voice(0)->asf(), snds3p->voice(0)->sz, &wav, &wavsz);
snd_bank::flush_cache(); snd_bank::dump("mf_wav.wav", wav, wavsz);
mf_broken::deinit(); free(wav);
} wav = nullptr;
wavsz = 0;
// AV codec (1:1 unmodified PCM data, other than resampling if enabled)
wav = (BYTE *) malloc(snd_2dx::assertsz_asf(snds3p->voice(0)->sz, avcodec::bmswac_resampler_f32));
avcodec::BmswTranscoderAsfToWav(snds3p->voice(0)->asf(), snds3p->voice(0)->sz, &wav, &wavsz, avcodec::bmswac_resampler_f32);
snd_bank::dump("ac_wav.wav", wav, wavsz);
free(wav);
wav = nullptr;
wavsz = 0;
// S3P=>2DX (max 4076 voices)
size_t i, j;
snd_2dx *snd2dx = snd_2dx::pack(id);
log_info("hooks::soundbank", "{}::Bank[2DX]::Name {}", id, snd2dx->header()->name);
for (i = 0; i < snds3p->header()->voice_cnt; i++)
{
// WMA=>WAV transcode
snd_s3p::bank::voice_hdr *voicesrc = snds3p->voice(i);
j = snd2dx->voice_emplace(snd_2dx::assertsz_asf(voicesrc->sz));
snd_2dx::bank::voice_hdr *voicedst = snd2dx->voice(j);
BYTE *voicedst_wave = voicedst->wave();
//mf_broken::asf_to_wav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz);
avcodec::BmswTranscoderAsfToWav(voicesrc->asf(), voicesrc->sz, &voicedst_wave, &voicedst->sz, snd_bank::resampler());
snd2dx->voice_ptr(j)->sz(voicedst); // updates voice_ptr table, do not skip
}
// Valid 2DX (trim is not necessary)
log_info("hooks::soundbank", "{}::Bank[2DX]::Dumping", id);
snd_2dx::bank *buf2dx = snd2dx->serialize(false);
snd_bank::dump("ac_soundbank.2dx", reinterpret_cast<const uint8_t *>(buf2dx), buf2dx->sz());
}
else
{
log_info("hooks::soundbank", "Tests failed");
}
// Caching and intermediate 2dx storage detection
BmsbEnumValidSoundbanks_t t = BmsbEnumValidSoundbanks;
BmsbEnumValidSoundbanks = [](void *, int32_t, int32_t) -> void * { return nullptr; };
char *tmp = (char *) malloc(0x3b8 + strlen("00000000") + 1);
int arr[6] = {25073, 24016, 26057, 25073, 26087, 29095};
strcpy(tmp, "SJISTITLE");
strcpy(tmp + 0xc0, "SJISARTIST");
strcpy(tmp + 0x3b8, "00000000");
for (int it: arr)
{
*(int32_t *) (tmp + 0x3b0) = it;
on_enum_valid_soundbanks(tmp, 3, 0);
}
free(tmp);
BmsbEnumValidSoundbanks = t;
snd_bank::flush_cache();
mf_broken::deinit();
}
} tests_("sndb_test");
void init(HINSTANCE hmodule, const char *model, const char *ext) void init(HINSTANCE hmodule, const char *model, const char *ext)
{ {
if (false && IsDebuggerPresent()) if (false && IsDebuggerPresent())
@@ -960,7 +965,7 @@ void init(HINSTANCE hmodule, const char *model, const char *ext)
// Setup on_enum_valid_ksbd and optional tests // Setup on_enum_valid_ksbd and optional tests
offset_.guard = new memutils::VProtectGuard((void *) offset_.RD_05d_2dx, SNDPATHFMTMAX); offset_.guard = new memutils::VProtectGuard((void *) offset_.RD_05d_2dx, SNDPATHFMTMAX);
detour::trampoline(reinterpret_cast<void *>(offset_.TX_BmsbEnumValidSoundbanks), reinterpret_cast<void *>(on_enum_valid_soundbanks), reinterpret_cast<void **>(&BmsbEnumValidSoundbanks)); detour::trampoline(reinterpret_cast<void *>(offset_.TX_BmsbEnumValidSoundbanks), reinterpret_cast<void *>(on_enum_valid_soundbanks), reinterpret_cast<void **>(&BmsbEnumValidSoundbanks));
hooks::devel::bind_routine(hooks::devel::sndb_test, run_tests); hooks::devel::bind_routine(&tests_);
log_info("hooks::soundbank", "Soundbank preprocessor ready"); log_info("hooks::soundbank", "Soundbank preprocessor ready");
} }
+24 -5
View File
@@ -55,7 +55,7 @@ MMRESULT __stdcall on_acmFormatSuggest(HANDLE had, LPWAVEFORMATEX pwfxSrc, LPWAV
} }
return r; return r;
} }
void run(HINSTANCE hmodule) void attach(HINSTANCE hmodule)
{ {
// Override acmFormatSuggest // Override acmFormatSuggest
HMODULE msacm = nullptr; HMODULE msacm = nullptr;
@@ -89,7 +89,7 @@ bool on_thum_exists_(void *titleUTF_getter, int32_t id, char flag)
return it->second; return it->second;
} }
void run(HINSTANCE hmodule) void attach(HINSTANCE hmodule)
{ {
// Reverse lookup function head (PUSH RDI) for ThumExists() specific signature, quietly fail if invalid // Reverse lookup function head (PUSH RDI) for ThumExists() specific signature, quietly fail if invalid
intptr_t beg, match, TX_ThumExists; intptr_t beg, match, TX_ThumExists;
@@ -107,18 +107,37 @@ void run(HINSTANCE hmodule)
namespace hooks::wmisc namespace hooks::wmisc
{ {
static bool patchmodel_ = true;
void skip_patching_by_model(bool val)
{
patchmodel_ = !val;
}
void apply_performance_hacks(HINSTANCE hmodule, const char *model, const char *ext) void apply_performance_hacks(HINSTANCE hmodule, const char *model, const char *ext)
{ {
if (!patchmodel_ && strcmp(model, "FORCE") != 0) return;
log_info("hooks::wmisc", "Enabling performance patches for {}-{}", model, ext); log_info("hooks::wmisc", "Enabling performance patches for {}-{}", model, ext);
switch (to_hash(model)) switch (to_hash(model))
{ {
case to_hash("LDJ"): case to_hash("LDJ"):
thumbnail::run(hmodule); thumbnail::attach(hmodule);
msacm::run(hmodule); msacm::attach(hmodule);
break;
case to_hash("FORCE"):
switch (to_hash(ext))
{
case to_hash("thumbnail"):
thumbnail::attach(hmodule);
break;
case to_hash("msacm"):
msacm::attach(hmodule);
break;
default:
break;
}
break; break;
default: default:
break; break;
} }
} }
} }
+1
View File
@@ -4,5 +4,6 @@
namespace hooks::wmisc namespace hooks::wmisc
{ {
void skip_patching_by_model(bool val);
void apply_performance_hacks(HINSTANCE hmodule, const char *model, const char *ext); void apply_performance_hacks(HINSTANCE hmodule, const char *model, const char *ext);
} }
+3
View File
@@ -2069,6 +2069,9 @@ int main_implementation(int argc, char *argv[]) {
// load game // load game
avs::game::load_dll(); avs::game::load_dll();
// devel attach
hooks::devel::attach();
// attach games // attach games
for (auto game : games) { for (auto game : games) {
game->attach(); game->attach();