From 4f810bfe9760b7ad10338247fad360f43a163d42 Mon Sep 17 00:00:00 2001 From: Ikey Doherty Date: Mon, 18 Dec 2017 18:01:07 +0000 Subject: [PATCH] redirect: Add workaround for the Unity Black Screen Of Nope If we detect access to a unity3D configuration file, we'll enter the unity3d workaround mode. Through this mode we'll abstract the real "prefs" file away into `/dev/shm` so that Unity games do not directly access that file again. If no original prefs file exists, we'll write a new prefs file with the absolute minimum requirement, i.e. disabling of fullscreen mode by default. If one DOES exist, we'll copy all of the prefs file **except** for the fullscreen option, and again, disabling it. Upon library exit, again, we'll duplicate the unity config to the real target file from the shm file, with the rewritten fullscreen option. The net effect is that we create a circle of consumption that forcibly disables unity games of a particular age (using `prefs` file) to not start in fullscreen mode, which almost always means one of two things: - Missing UI text elements - Unity3D clamps resolution to 0x0 Many ports aren't going to see updates to their Unity build for a long time, or indeed, ever. Instead we work around this in LSI and sit in between the game and the configuration file. Signed-off-by: Ikey Doherty --- src/redirect/main.c | 323 +++++++++++++++++++++++++++++++++++++-- src/redirect/meson.build | 2 + 2 files changed, 316 insertions(+), 9 deletions(-) diff --git a/src/redirect/main.c b/src/redirect/main.c index b1a9df5..a713d1d 100644 --- a/src/redirect/main.c +++ b/src/redirect/main.c @@ -12,13 +12,17 @@ #define _GNU_SOURCE #include +#include +#include #include #include #include #include #include #include +#include #include +#include #include "../common/common.h" #include "../common/files.h" @@ -36,6 +40,33 @@ .func = (void **)(&lsi_table.x), .func_size = sizeof(lsi_table.x) \ } +/** + * Cheap and cheerful, this is our default fake unity3d config which we write + * out to trick unity3d games into reading a non broken config on Linux. + * This forces the equivalent of "-screen-fullscreen 0" because the older + * Unity builds will set fullscreen to 1, but they'll also set the resolution + * width and height to 0 (which is then clamped) + */ +static const char *unity3d_config = + "\ +\n\ + 0\n\ +\n\ +"; + +/** + * When we rewrite the config file in place we'll repllace the "Is Fullscreen mode" + * line with this one so that we forcibly disable it. + */ +static const char *unity3d_snippet = + " 0\n"; + +static void lsi_maybe_init_unity3d(const char *p); +static void lsi_unity_backup_config(void); +static FILE *lsi_unity_redirect(const char *p, const char *modes); +static FILE *lsi_unity_get_config_file(const char *modes); +static void lsi_unity_trim_copy_config(FILE *from, FILE *to); + /** * Whether we've initialised yet or not. */ @@ -73,6 +104,16 @@ typedef struct LsiRedirectTable { struct { void *libc; } handles; + + /* Our shm_open() unity3d redirect.. */ + struct { + char *original_config_path; + char *config_path; + char *shm_path; + bool enabled; + bool failed; + bool had_init; + } unity3d; } LsiRedirectTable; /** @@ -119,6 +160,22 @@ __attribute__((destructor)) static void lsi_redirect_shutdown(void) } lsi_init = false; + if (lsi_table.unity3d.original_config_path) { + lsi_unity_backup_config(); + free(lsi_table.unity3d.original_config_path); + lsi_table.unity3d.original_config_path = NULL; + } + if (lsi_table.unity3d.config_path) { + free(lsi_table.unity3d.config_path); + lsi_table.unity3d.config_path = NULL; + } + + if (lsi_table.unity3d.shm_path) { + (void)shm_unlink(lsi_table.unity3d.shm_path); + free(lsi_table.unity3d.shm_path); + lsi_table.unity3d.shm_path = NULL; + } + if (lsi_table.handles.libc) { dlclose(lsi_table.handles.libc); lsi_table.handles.libc = NULL; @@ -204,6 +261,7 @@ __attribute__((constructor)) static void lsi_redirect_init(void) autofree(char) *process_name = NULL; char **paths = NULL; char **orig = NULL; + autofree(char) *xdg_config_dir = NULL; /* Absolute path to the process for fine-grained matching */ process_name = lsi_get_process_name(); @@ -213,7 +271,27 @@ __attribute__((constructor)) static void lsi_redirect_init(void) return; } - /* Grab the stam installation directories */ + /* Ensure we know the path to unity3d config */ + xdg_config_dir = lsi_get_user_config_dir(); + if (asprintf(&lsi_table.unity3d.config_path, "%s/unity3d", xdg_config_dir) < 0) { + fputs("OOM\n", stderr); + abort(); + } + + /* shm path for our fake unity3d config path */ + if (asprintf(&lsi_table.unity3d.shm_path, + "/u%d-LinuxSteamIntegration.unity3d.%d", + getuid(), + getpgrp()) < 0) { + fputs("OOM\n", stderr); + abort(); + } + + /* Symbolic, we don't just use this for anyone, yknow. :P */ + lsi_table.unity3d.enabled = false; + lsi_table.unity3d.failed = false; + + /* Grab the steam installation directories */ orig = paths = lsi_get_steam_paths(); /* For each path try to form a valid profile for the process name and Steam directory */ @@ -288,6 +366,157 @@ static char *lsi_get_redirect_path(const char *syscall_id, LsiRedirectOperation return NULL; } +/** + * The unity3d config path has been accessed, so we need to turn on our + * workaround for unity3d prefs + */ +static void lsi_maybe_init_unity3d(const char *p) +{ + if (lsi_table.unity3d.enabled) { + return; + } + + if (lsi_table.unity3d.config_path && !strstr(p, lsi_table.unity3d.config_path)) { + return; + } + + /* We're in action */ + lsi_table.unity3d.enabled = true; + lsi_log_set_id("unity3d"); + lsi_log_error("Activating \"black screen of nope\" workaround"); +} + +static inline bool str_has_prefix(const char *a, const char *b) +{ + size_t lena, lenb; + lena = strlen(a); + lenb = strlen(b); + if (lenb > lena) { + return false; + } + return strncmp(a, b, lenb) == 0; +} + +/** + * Determine if the input path is a unity3d "prefs" file + */ +static bool is_unity3d_prefs_file(const char *p) +{ + autofree(char) *clone = NULL; + char *basenom = NULL; + + /* Must only happen when enabled */ + if (!lsi_table.unity3d.enabled) { + return false; + } + + /* We found it already */ + if (lsi_table.unity3d.original_config_path) { + return false; + } + + /* Must be in config path */ + if (!str_has_prefix(p, lsi_table.unity3d.config_path)) { + return false; + } + + clone = strdup(p); + if (!clone) { + return false; + } + basenom = basename(clone); + if (!basenom) { + return false; + } + + /* Must be called "prefs" */ + return strcmp(basenom, "prefs") == 0 ? true : false; +} + +/** + * Copy the config from the @from file to the @to file, which + * will also strip out any "evil" lines, i.e. the fullscreen + * stanza. + * + * If the input file does not exist, the default configuration + * will be used. + */ +static void lsi_unity_trim_copy_config(FILE *from, FILE *to) +{ + char *buf = NULL; + size_t sn = 0; + ssize_t r = 0; + + /* No input? Write the default configuration then */ + if (!from) { + if (fwrite(unity3d_config, strlen(unity3d_config), 1, to) != 1) { + lsi_log_error("Failed to create initial Unity3D config: %s", + strerror(errno)); + } + return; + } + + /* Have an input file, write to the output but omit all the bad lines */ + while ((r = getline(&buf, &sn, from)) != -1) { + const char *to_write = NULL; + + /* Rewrite this line, it breaks games. */ + if (strstr(buf, "Screenmanager Is Fullscreen mode")) { + to_write = unity3d_snippet; + r = strlen(to_write); + } else { + to_write = buf; + } + + if (fwrite(to_write, r, 1, to) != 1) { + lsi_log_error("Failed to write Unity3D config: %s", strerror(errno)); + goto failed; + } + + if (buf) { + free(buf); + buf = NULL; + } + } + + fflush(to); + +failed: + if (buf) { + free(buf); + } +} + +/** + * Clone the existing shm configuration into the real config path, and copy + * all lines that we "like" + * + * Basically this ensures we force the dumped config to omit the fullscreen + * options as they break so many games. + */ +static void lsi_unity_backup_config() +{ + autofree(FILE) *shm_file = NULL; + autofree(FILE) *dest_file = NULL; + + if (!lsi_table.unity3d.enabled || !lsi_table.unity3d.original_config_path) { + return; + } + + shm_file = lsi_unity_get_config_file("r"); + if (!shm_file) { + return; + } + + dest_file = lsi_table.fopen64(lsi_table.unity3d.original_config_path, "w"); + if (!dest_file) { + return; + } + + lsi_log_debug("Saved Unity3D config to %s", lsi_table.unity3d.original_config_path); + lsi_unity_trim_copy_config(shm_file, dest_file); +} + _nica_public_ int open(const char *p, int flags, ...) { va_list va; @@ -305,16 +534,84 @@ _nica_public_ int open(const char *p, int flags, ...) mode = va_arg(va, mode_t); va_end(va); + lsi_maybe_init_unity3d(p); + /* Not interested in this guy apparently */ if (!lsi_override) { - return lsi_table.open(p, flags, mode); + goto fallback_open; } replacement = lsi_get_redirect_path("open", op, p); - if (!replacement) { - return lsi_table.open(p, flags, mode); + if (replacement) { + return lsi_table.open(replacement, flags, mode); } - return lsi_table.open(replacement, flags, mode); + +fallback_open: + return lsi_table.open(p, flags, mode); +} + +/** + * Get the shm file as a FILE stream + */ +static FILE *lsi_unity_get_config_file(const char *modes) +{ + int perm = O_RDONLY; + + if (strstr(modes, "w")) { + perm = O_RDWR | O_CREAT | O_TRUNC; + } + + /* Attempt to shm_open our fake path */ + int fd = shm_open(lsi_table.unity3d.shm_path, perm, 0666); + if (!fd) { + return NULL; + } + return fdopen(fd, modes); +} + +/** + * We now need to initialise the unity3d config if we haven't done so + */ +static void lsi_unity_init_config(void) +{ + autofree(FILE) *source = NULL; + autofree(FILE) *dest = NULL; + + if (lsi_table.unity3d.had_init || lsi_table.unity3d.failed) { + return; + } + + lsi_table.unity3d.had_init = true; + + dest = lsi_unity_get_config_file("w"); + source = lsi_table.fopen64(lsi_table.unity3d.original_config_path, "r"); + + lsi_unity_trim_copy_config(source, dest); +} + +/** + * Redirect requests for non-existant config files in Unity3D to a temporary + * shm file which we'll write our initial "sane" configuration to. + */ +static FILE *lsi_unity_redirect(const char *p, const char *modes) +{ + FILE *ret = NULL; + + /* Preserve this path for later cloning.. */ + lsi_table.unity3d.original_config_path = strdup(p); + + lsi_unity_init_config(); + + ret = lsi_unity_get_config_file(modes); + if (!ret) { + return NULL; + } + + lsi_log_info("fopen64(%s): Redirecting unity config '%s' to shm(%s)", + modes, + p, + lsi_table.unity3d.shm_path); + return ret; } _nica_public_ FILE *fopen64(const char *p, const char *modes) @@ -327,16 +624,24 @@ _nica_public_ FILE *fopen64(const char *p, const char *modes) */ lsi_redirect_init_tables(); + lsi_maybe_init_unity3d(p); + /* Not interested in this guy apparently */ if (!lsi_override) { - return lsi_table.fopen64(p, modes); + goto fallback_open; } replacement = lsi_get_redirect_path("fopen64", op, p); - if (!replacement) { - return lsi_table.fopen64(p, modes); + if (replacement) { + return lsi_table.fopen64(replacement, modes); } - return lsi_table.fopen64(replacement, modes); + +fallback_open: + + if (is_unity3d_prefs_file(p)) { + return lsi_unity_redirect(p, modes); + } + return lsi_table.fopen64(p, modes); } /* diff --git a/src/redirect/meson.build b/src/redirect/meson.build index 75b531e..6bae786 100644 --- a/src/redirect/meson.build +++ b/src/redirect/meson.build @@ -4,6 +4,7 @@ if with_libredirect == true # Might not need libdl for alt libcs libdl = meson.get_compiler('c').find_library('dl', required : false) + librt = meson.get_compiler('c').find_library('rt', required : false) redirect_sources = [ 'main.c', @@ -22,6 +23,7 @@ if with_libredirect == true include_directories: nica_includes, dependencies: [ libdl, + librt, link_lsi_common, ], install: true,