Skip to content

Commit 33e544d

Browse files
committed
thcrap: try to automatically create a portable installation when possible
There are 2 major things that prevents thcrap from being portable: the shortcuts have absolute paths, and we have paths pointing towards the games. For paths pointing towards the games, we'll make them relative instead of absolute if the games are close enough to thcrap and both are likely to be moveable together. For shortcuts, we'll replace them with small executables that run thcrap_loader with a relative path and the correct arguments when they're either in thcrap's directory or in the games' directory. We'll also always use them on wine, because wine tends to have some problems with the shortcuts (especially if you want to configure your Steam Deck to run the games from your Steam library).
1 parent 1024b13 commit 33e544d

File tree

12 files changed

+195
-61
lines changed

12 files changed

+195
-61
lines changed

thcrap/src/search.cpp

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,44 @@ struct search_thread_param
3030

3131
static bool searchCancelled = false;
3232

33+
template<typename T>
34+
static size_t iter_size(T begin, T end)
35+
{
36+
size_t size = 0;
37+
while (begin != end) {
38+
size++;
39+
begin++;
40+
}
41+
return size;
42+
}
43+
44+
// If thcrap and the game are likely to be moved together, we want a relative path,
45+
// otherwise we want an absolute path.
46+
char *SearchDecideStoredPathForm(std::filesystem::path target, std::filesystem::path self)
47+
{
48+
if (!self.is_absolute()) {
49+
self = std::filesystem::absolute(self);
50+
}
51+
if (!target.is_absolute()) {
52+
target = std::filesystem::absolute(target);
53+
}
54+
55+
if (target.root_path() != self.root_path()) {
56+
return strdup(target.u8string().c_str());
57+
}
58+
59+
auto [self_diff, target_diff] = std::mismatch(self.begin(), self.end(), target.begin(), target.end());
60+
// These numbers were decided arbitrarly by making a list of path examples that seemed
61+
// to make sense and looking at which values they need.
62+
// These examples are available in the unit tests.
63+
if (iter_size(self_diff, self.end()) <= 3 && iter_size(target_diff, target.end()) <= 2) {
64+
return strdup(target.lexically_relative(self).u8string().c_str());
65+
}
66+
else {
67+
return strdup(target.u8string().c_str());
68+
}
69+
}
70+
3371
static int SearchCheckExe(search_state_t& state, const fs::directory_entry &ent)
3472
{
3573
int ret = 0;
@@ -71,7 +109,7 @@ static int SearchCheckExe(search_state_t& state, const fs::directory_entry &ent)
71109
}
72110

73111
game_search_result result = std::move(*ver);
74-
result.path = strdup(exe_fn.c_str());
112+
result.path = SearchDecideStoredPathForm(ent.path(), std::filesystem::current_path());
75113
result.description = strdup(description.c_str());
76114
identify_free(ver);
77115

thcrap/src/search.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,17 @@ THCRAP_API void SearchForGames_cancel();
6565

6666
// Free the return of SearchForGames
6767
THCRAP_API void SearchForGames_free(game_search_result *games);
68+
69+
#ifdef __cplusplus
70+
71+
namespace std
72+
{
73+
namespace filesystem
74+
{
75+
class path;
76+
};
77+
};
78+
79+
// Exported for testing only
80+
char *SearchDecideStoredPathForm(std::filesystem::path target, std::filesystem::path self);
81+
#endif

thcrap/src/shelllink.cpp

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ std::filesystem::path get_link_dir(ShortcutsDestination destination, const std::
278278
std::filesystem::path GetIconPath(const char *icon_path_, const char *game_id)
279279
{
280280
auto icon_path = std::filesystem::u8path(icon_path_);
281+
if (!icon_path.is_absolute()) {
282+
icon_path = std::filesystem::absolute(icon_path);
283+
}
281284

282285
if (icon_path.filename() != "vpatch.exe")
283286
return icon_path;
@@ -298,27 +301,53 @@ std::filesystem::path GetIconPath(const char *icon_path_, const char *game_id)
298301
return icon_path;
299302
}
300303

304+
ShortcutsType DecideAutoShortcutType(ShortcutsDestination destination, const std::filesystem::path& link_dir, const std::filesystem::path& thcrap_dir_relative)
305+
{
306+
if (destination == SHDESTINATION_DESKTOP || destination == SHDESTINATION_START_MENU) {
307+
// Assume thcrap and the shortcut are in completely different emplacements.
308+
// Both are unlikely to be moved.
309+
if (OS_is_wine()) {
310+
// Wine doesn't work really well with shortcuts
311+
return SHTYPE_WRAPPER_ABSPATH;
312+
}
313+
return SHTYPE_SHORTCUT;
314+
}
315+
else if (destination == SHDESTINATION_THCRAP_DIR || destination == SHDESTINATION_GAMES_DIRECTORY) {
316+
// SHDESTINATION_THCRAP_DIR:
317+
// The shortcuts are part of thcrap, and if thcrap is moved, the shortcuts
318+
// will be moved with it.
319+
// Unless the user wants to move the generated shortcuts. But we can't know
320+
// what the user will do at this point, so we have to make a choice, and this
321+
// one seems better than trying to be smart and unpredictable.
322+
323+
// SHDESTINATION_GAMES_DIRECTORY:
324+
// The shortcuts are part of the games. If the games move and thcrap doesn't move,
325+
// then the games' paths in games.js become broken.
326+
// So we can assume that these shortcuts will always be moved with thcrap, and making
327+
// them relative makes more sense and helps with portable installations.
328+
return SHTYPE_WRAPPER_RELPATH;
329+
}
330+
TH_UNREACHABLE;
331+
}
332+
301333
int CreateShortcuts(const char *run_cfg_fn, games_js_entry *games, ShortcutsDestination destination, ShortcutsType shortcut_type)
302334
{
303335
LPCWSTR loader_exe = L"thcrap_loader" DEBUG_OR_RELEASE_W L".exe";
304336
auto thcrap_dir = GetThcrapDir();
305337
auto self_path = thcrap_dir / L"bin" / loader_exe;
306338
auto link_dir = get_link_dir(destination, thcrap_dir);
307339
int ret = 0;
340+
// Yay, COM.
341+
HRESULT com_init_succeeded = E_FAIL;
308342

309-
if (shortcut_type != SHTYPE_SHORTCUT &&
343+
if (shortcut_type != SHTYPE_AUTO &&
344+
shortcut_type != SHTYPE_SHORTCUT &&
310345
shortcut_type != SHTYPE_WRAPPER_ABSPATH &&
311346
shortcut_type != SHTYPE_WRAPPER_RELPATH) {
312347
log_print("Error creating shortcuts: invalid parameter for shortcut_type. Please report this error to the developpers.\n");
313348
return 1;
314349
}
315350

316-
// Yay, COM.
317-
HRESULT com_init_succeded = E_FAIL;
318-
if (shortcut_type == SHTYPE_SHORTCUT) {
319-
com_init_succeded = CoInitializeEx(NULL, COINIT_MULTITHREADED);
320-
}
321-
322351
log_printf("Creating shortcuts");
323352

324353
for (size_t i = 0; games[i].id; i++) {
@@ -334,16 +363,24 @@ int CreateShortcuts(const char *run_cfg_fn, games_js_entry *games, ShortcutsDest
334363
auto link_args_w = std::make_unique<wchar_t[]>(link_args.length() + 1);
335364
StringToUTF16(link_args_w.get(), link_args.c_str(), -1);
336365

337-
if (shortcut_type == SHTYPE_SHORTCUT) {
366+
ShortcutsType local_shortcut_type = shortcut_type;
367+
if (local_shortcut_type == SHTYPE_AUTO) {
368+
local_shortcut_type = DecideAutoShortcutType(destination, link_dir, thcrap_dir);
369+
}
370+
371+
if (local_shortcut_type == SHTYPE_SHORTCUT) {
372+
if (com_init_succeeded == E_FAIL) {
373+
com_init_succeeded = CoInitializeEx(NULL, COINIT_MULTITHREADED);
374+
}
338375
link_path.replace_extension("lnk");
339376
if (CreateLink(link_path, self_path, link_args_w.get(), thcrap_dir, icon_path)) {
340377
ret = 1;
341378
}
342379
}
343-
else if (shortcut_type == SHTYPE_WRAPPER_ABSPATH || shortcut_type == SHTYPE_WRAPPER_RELPATH) {
380+
else if (local_shortcut_type == SHTYPE_WRAPPER_ABSPATH || local_shortcut_type == SHTYPE_WRAPPER_RELPATH) {
344381
link_path.replace_extension("exe");
345382
auto exe_args = std::wstring(loader_exe) + L" " + link_args_w.get();
346-
if (!CreateWrapper(link_path, thcrap_dir, loader_exe, exe_args, icon_path, shortcut_type)) {
383+
if (!CreateWrapper(link_path, thcrap_dir, loader_exe, exe_args, icon_path, local_shortcut_type)) {
347384
ret = 1;
348385
}
349386
}
@@ -359,7 +396,7 @@ int CreateShortcuts(const char *run_cfg_fn, games_js_entry *games, ShortcutsDest
359396
}
360397
}
361398

362-
if (com_init_succeded == S_OK) {
399+
if (com_init_succeeded == S_OK) {
363400
CoUninitialize();
364401
}
365402
return ret;

thcrap/src/shelllink.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ enum ShortcutsDestination
1919

2020
enum ShortcutsType
2121
{
22+
SHTYPE_AUTO = 0,
2223
SHTYPE_SHORTCUT = 1,
2324
SHTYPE_WRAPPER_ABSPATH = 2,
2425
SHTYPE_WRAPPER_RELPATH = 3,

thcrap/thcrap_x86.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ EXPORTS
313313
SearchForGamesInstalled
314314
SearchForGames_cancel
315315
SearchForGames_free
316+
SearchDecideStoredPathForm
316317

317318
; Shortcuts
318319
; ---------

thcrap_configure/src/configure.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ int TH_CDECL win32_utf8_main(int argc, const char *argv[])
340340

341341
if (console_ask_yn(_A("Create shortcuts? (required for first run)")) != 'n') {
342342
gamesArray = games_js_to_array(games);
343-
if (CreateShortcuts(run_cfg_fn.c_str(), gamesArray, SHDESTINATION_THCRAP_DIR, SHTYPE_SHORTCUT) != 0) {
343+
if (CreateShortcuts(run_cfg_fn.c_str(), gamesArray, SHDESTINATION_THCRAP_DIR, SHTYPE_AUTO) != 0) {
344344
goto end;
345345
}
346346
}

thcrap_configure_v3/Page5.xaml.cs

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,6 @@ public partial class Page5 : UserControl
2525
{
2626
bool? isUTLPresent = null;
2727
GlobalConfig config = null;
28-
ThcrapDll.ShortcutsType shortcutsType = ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT;
29-
const long configDestinationMask = 0xFFFF;
30-
const int configTypeOffset = 16;
31-
32-
[Flags]
33-
public enum ShortcutDestinations
34-
{
35-
Desktop = 1,
36-
StartMenu = 2,
37-
GamesFolder = 4,
38-
ThcrapFolder = 8,
39-
}
4028

4129
public Page5()
4230
{
@@ -54,17 +42,12 @@ public void Enter()
5442
if (config == null)
5543
{
5644
config = new GlobalConfig();
57-
ShortcutDestinations dest = (ShortcutDestinations)(config.default_shortcut_destinations & configDestinationMask);
45+
GlobalConfig.ShortcutDestinations dest = config.default_shortcut_destinations;
5846

59-
checkboxDesktop.IsChecked = (dest & ShortcutDestinations.Desktop) != 0;
60-
checkboxStartMenu.IsChecked = (dest & ShortcutDestinations.StartMenu) != 0;
61-
checkboxGamesFolder.IsChecked = (dest & ShortcutDestinations.GamesFolder) != 0;
62-
checkboxThcrapFolder.IsChecked = (dest & ShortcutDestinations.ThcrapFolder) != 0;
63-
shortcutsType = (ThcrapDll.ShortcutsType)(config.default_shortcut_destinations >> configTypeOffset);
64-
if (shortcutsType != ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT &&
65-
shortcutsType != ThcrapDll.ShortcutsType.SHTYPE_WRAPPER_ABSPATH &&
66-
shortcutsType != ThcrapDll.ShortcutsType.SHTYPE_WRAPPER_RELPATH)
67-
shortcutsType = ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT;
47+
checkboxDesktop.IsChecked = dest.HasFlag(GlobalConfig.ShortcutDestinations.Desktop);
48+
checkboxStartMenu.IsChecked = dest.HasFlag(GlobalConfig.ShortcutDestinations.StartMenu);
49+
checkboxGamesFolder.IsChecked = dest.HasFlag(GlobalConfig.ShortcutDestinations.GamesFolder);
50+
checkboxThcrapFolder.IsChecked = dest.HasFlag(GlobalConfig.ShortcutDestinations.ThcrapFolder);
6851
}
6952
}
7053

@@ -101,17 +84,16 @@ private void CreateShortcuts(string configName, IEnumerable<ThcrapDll.games_js_e
10184
}
10285
gamesArray[i] = new ThcrapDll.games_js_entry();
10386

104-
ThcrapDll.CreateShortcuts(configName, gamesArray, destination, shortcutsType);
87+
ThcrapDll.CreateShortcuts(configName, gamesArray, destination, config.shortcuts_type);
10588
}
10689

10790
public void Leave(string configName, IEnumerable<ThcrapDll.games_js_entry> games)
10891
{
109-
ShortcutDestinations shortcutDestinations =
110-
(checkboxDesktop.IsChecked == true ? ShortcutDestinations.Desktop : 0) |
111-
(checkboxStartMenu.IsChecked == true ? ShortcutDestinations.StartMenu : 0) |
112-
(checkboxGamesFolder.IsChecked == true ? ShortcutDestinations.GamesFolder : 0) |
113-
(checkboxThcrapFolder.IsChecked == true ? ShortcutDestinations.ThcrapFolder : 0);
114-
config.default_shortcut_destinations = (long)shortcutDestinations | ((long)shortcutsType << configTypeOffset);
92+
config.default_shortcut_destinations =
93+
(checkboxDesktop.IsChecked == true ? GlobalConfig.ShortcutDestinations.Desktop : 0) |
94+
(checkboxStartMenu.IsChecked == true ? GlobalConfig.ShortcutDestinations.StartMenu : 0) |
95+
(checkboxGamesFolder.IsChecked == true ? GlobalConfig.ShortcutDestinations.GamesFolder : 0) |
96+
(checkboxThcrapFolder.IsChecked == true ? GlobalConfig.ShortcutDestinations.ThcrapFolder : 0);
11597
config.Save();
11698

11799
if (checkboxDesktop.IsChecked == true)

thcrap_configure_v3/Runconfig.cs

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,40 +35,63 @@ public void Save(string config_name)
3535
}
3636
class GlobalConfig
3737
{
38+
private const long configDestinationMask = 0xFFFF;
39+
private const int configTypeOffset = 16;
40+
41+
[Flags]
42+
public enum ShortcutDestinations
43+
{
44+
Desktop = 1,
45+
StartMenu = 2,
46+
GamesFolder = 4,
47+
ThcrapFolder = 8,
48+
}
49+
3850
public bool background_updates { get; set; }
3951
public long time_between_updates { get; set; }
4052
public bool update_at_exit { get; set; }
4153
public bool update_others { get; set; }
4254
public bool console { get; set; }
4355
public long exception_detail { get; set; }
4456
public long codepage { get; set; }
45-
public long default_shortcut_destinations { get; set; }
57+
public ShortcutDestinations default_shortcut_destinations { get; set; }
58+
public ThcrapDll.ShortcutsType shortcuts_type { get; set; }
4659

4760
public GlobalConfig()
4861
{
49-
background_updates = ThcrapDll.globalconfig_get_boolean("background_updates", false);
50-
time_between_updates = ThcrapDll.globalconfig_get_integer("time_between_updates", 5);
51-
update_at_exit = ThcrapDll.globalconfig_get_boolean("update_at_exit", false);
52-
update_others = ThcrapDll.globalconfig_get_boolean("update_others", true);
53-
console = ThcrapDll.globalconfig_get_boolean("console", false);
54-
exception_detail = ThcrapDll.globalconfig_get_integer("exception_detail", 1);
55-
codepage = ThcrapDll.globalconfig_get_integer("codepage", 932);
56-
default_shortcut_destinations = ThcrapDll.globalconfig_get_integer("default_shortcut_destinations",
57-
(long)(Page5.ShortcutDestinations.Desktop | Page5.ShortcutDestinations.StartMenu));
62+
background_updates = ThcrapDll.globalconfig_get_boolean("background_updates", false);
63+
time_between_updates = ThcrapDll.globalconfig_get_integer("time_between_updates", 5);
64+
update_at_exit = ThcrapDll.globalconfig_get_boolean("update_at_exit", false);
65+
update_others = ThcrapDll.globalconfig_get_boolean("update_others", true);
66+
console = ThcrapDll.globalconfig_get_boolean("console", false);
67+
exception_detail = ThcrapDll.globalconfig_get_integer("exception_detail", 1);
68+
codepage = ThcrapDll.globalconfig_get_integer("codepage", 932);
69+
70+
long _default_shortcut_destinations = ThcrapDll.globalconfig_get_integer("default_shortcut_destinations",
71+
(long)(ShortcutDestinations.Desktop | ShortcutDestinations.StartMenu));
72+
default_shortcut_destinations = (ShortcutDestinations)(_default_shortcut_destinations & configDestinationMask);
73+
shortcuts_type = (ThcrapDll.ShortcutsType)(_default_shortcut_destinations >> configTypeOffset);
74+
if (shortcuts_type != ThcrapDll.ShortcutsType.SHTYPE_AUTO &&
75+
shortcuts_type != ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT &&
76+
shortcuts_type != ThcrapDll.ShortcutsType.SHTYPE_WRAPPER_ABSPATH &&
77+
shortcuts_type != ThcrapDll.ShortcutsType.SHTYPE_WRAPPER_RELPATH)
78+
shortcuts_type = ThcrapDll.ShortcutsType.SHTYPE_SHORTCUT;
5879
}
5980
public void Save()
6081
{
6182
if (!Directory.Exists("config"))
6283
Directory.CreateDirectory("config");
6384

64-
ThcrapDll.globalconfig_set_boolean("background_updates", background_updates);
65-
ThcrapDll.globalconfig_set_integer("time_between_updates", time_between_updates);
66-
ThcrapDll.globalconfig_set_boolean("update_at_exit", update_at_exit);
67-
ThcrapDll.globalconfig_set_boolean("update_others", update_others);
68-
ThcrapDll.globalconfig_set_boolean("console", console);
69-
ThcrapDll.globalconfig_set_integer("exception_detail", exception_detail);
70-
ThcrapDll.globalconfig_set_integer("default_shortcut_destinations", (long)default_shortcut_destinations);
71-
ThcrapDll.globalconfig_set_integer("codepage", codepage);
85+
ThcrapDll.globalconfig_set_boolean("background_updates", background_updates);
86+
ThcrapDll.globalconfig_set_integer("time_between_updates", time_between_updates);
87+
ThcrapDll.globalconfig_set_boolean("update_at_exit", update_at_exit);
88+
ThcrapDll.globalconfig_set_boolean("update_others", update_others);
89+
ThcrapDll.globalconfig_set_boolean("console", console);
90+
ThcrapDll.globalconfig_set_integer("exception_detail", exception_detail);
91+
ThcrapDll.globalconfig_set_integer("codepage", codepage);
92+
93+
long _default_shortcut_destinations = (long)default_shortcut_destinations | ((long)shortcuts_type << configTypeOffset);
94+
ThcrapDll.globalconfig_set_integer("default_shortcut_destinations", _default_shortcut_destinations);
7295
}
7396
}
7497
}

thcrap_configure_v3/ThcrapDll.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ public enum ShortcutsDestination
196196

197197
public enum ShortcutsType
198198
{
199+
SHTYPE_AUTO = 0,
199200
SHTYPE_SHORTCUT = 1,
200201
SHTYPE_WRAPPER_ABSPATH = 2,
201202
SHTYPE_WRAPPER_RELPATH = 3,

thcrap_test/src/search.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#include "thcrap.h"
2+
#include <filesystem>
3+
#include "gtest/gtest.h"
4+
5+
template<typename T>
6+
static size_t iter_size(T begin, T end)
7+
{
8+
size_t size = 0;
9+
while (begin != end) {
10+
size++;
11+
begin++;
12+
}
13+
return size;
14+
}
15+
16+
static void test(const char *self, const char *target, const char *expected)
17+
{
18+
if (expected == nullptr) {
19+
expected = target;
20+
}
21+
22+
char *ret = SearchDecideStoredPathForm(target, self);
23+
EXPECT_STREQ(ret, expected);
24+
free(ret);
25+
}
26+
27+
TEST(Search, SearchDecideStoredPathForm)
28+
{
29+
test("C:\\a\\1\\2\\3\\4\\5\\6\\", "C:\\b\\1\\2\\3\\4\\5\\6", nullptr);
30+
test("D:\\thcrap\\", "E:\\th07\\th07.exe", nullptr);
31+
test("C:\\Users\\brliron\\Downlods\\thcrap\\", "C:\\Program Files\\Steam\\common\\th07\\th07.exe", nullptr);
32+
test("C:\\Users\\brliron\\Desktop\\games\\tools\\thcrap\\", "C:\\Users\\brliron\\Desktop\\games\\th07\\th07.exe", "..\\..\\th07\\th07.exe");
33+
test("C:\\Users\\brliron\\Desktop\\games\\th07\\thcrap\\", "C:\\Users\\brliron\\Desktop\\games\\th07\\th07.exe", "..\\th07.exe");
34+
test("C:\\Users\\brliron\\Desktop\\games\\", "C:\\Users\\brliron\\Desktop\\games\\th07\\th07.exe", "th07\\th07.exe");
35+
test("D:\\thcrap\\", "D:\\th07\\th07.exe", "..\\th07\\th07.exe");
36+
}

0 commit comments

Comments
 (0)