From 469d6e2f4889dfe779e0cb7be62cf3f92bad5f86 Mon Sep 17 00:00:00 2001 From: Samuel P Date: Fri, 2 Aug 2019 20:38:33 +0200 Subject: [PATCH] Switch homebrew port (#173) * Initial port * Switch specific video modes * Handle clean exit * Hide option to disable gamepads, force it enabled * Match buttons layout with Switch controllers * Hide player name setting (to avoid getting stuck) * Switch specific VFS setup instead of env bootstrap * Clean up, get rid of warnings and a few ifdefs * Add Switch specific build script and assets * Make vfs setup mount packages * Re-enable packaging on Switch * Transpile shaders to es and install them * Add applet warning and shader deps to README * Remove script; instead using meson build scripts * Strict prototypes * Build script compat with project minimum meson ver Refactor of pack.py exclude option * Uniformise header inclusion on arch_switch.c * Allow input for any dev; hide the option on Switch * Silence unsused function warnings --- meson.build | 8 +- resources/00-taisei.pkgdir/shader/meson.build | 2 +- resources/meson.build | 26 +++++ scripts/pack.py | 9 +- src/arch_switch.c | 107 ++++++++++++++++++ src/arch_switch.h | 22 ++++ src/config.c | 5 + src/gamepad.h | 7 ++ src/global.h | 5 + src/log.c | 3 +- src/menu/options.c | 8 ++ src/meson.build | 53 +++++++++ src/renderer/meson.build | 32 ++++-- src/util/compat.h | 7 ++ src/vfs/meson.build | 5 + src/vfs/setup.h | 44 +++++++ src/vfs/setup_emscripten.c | 32 ++---- src/vfs/setup_switch.c | 39 +++++++ src/video.c | 17 ++- src/video.h | 1 + switch/README.md | 43 +++++++ switch/crossfile.sh | 44 +++++++ switch/icon.jpg | Bin 0 -> 25194 bytes switch/meson.build | 4 + 24 files changed, 477 insertions(+), 46 deletions(-) create mode 100644 src/arch_switch.c create mode 100644 src/arch_switch.h create mode 100644 src/vfs/setup_switch.c create mode 100644 switch/README.md create mode 100755 switch/crossfile.sh create mode 100644 switch/icon.jpg create mode 100644 switch/meson.build diff --git a/meson.build b/meson.build index dcf6e441..21c67b17 100644 --- a/meson.build +++ b/meson.build @@ -117,7 +117,7 @@ if sm_check.stderr() != '' warning('Submodule check completed with errors:\n@0@'.format(sm_check.stderr())) endif -static = get_option('static') or (host_machine.system() == 'emscripten') +static = get_option('static') or ['emscripten', 'nx'].contains(host_machine.system()) dep_freetype = dependency('freetype2', required : true, static : static, fallback : ['freetype', 'freetype_dep']) dep_opusfile = dependency('opusfile', required : false, static : static, fallback : ['opusfile', 'opusfile_dep']) @@ -197,6 +197,7 @@ prefer_relpath_systems = [ force_relpath_systems = [ 'emscripten', + 'nx' ] if macos_app_bundle @@ -268,7 +269,7 @@ config.set('TAISEI_BUILDCONF_LOG_FATAL_MSGBOX', ( )) config.set('TAISEI_BUILDCONF_DEBUG_OPENGL', get_option('debug_opengl')) -install_docs = get_option('docs') and host_machine.system() != 'emscripten' +install_docs = get_option('docs') and not ['emscripten', 'nx'].contains(host_machine.system()) if host_machine.system() == 'windows' if install_docs @@ -310,6 +311,7 @@ bindist_deps = [] subdir('misc') subdir('emscripten') +subdir('switch') subdir('external') subdir('resources') subdir('doc') @@ -363,7 +365,7 @@ Summary: ', '.join(enabled_audio_backends), get_option('a_default'), ', '.join(enabled_renderers), - get_option('r_default'), + default_renderer, get_option('shader_transpiler'), enable_zip, package_data, diff --git a/resources/00-taisei.pkgdir/shader/meson.build b/resources/00-taisei.pkgdir/shader/meson.build index f4235b93..80abe613 100644 --- a/resources/00-taisei.pkgdir/shader/meson.build +++ b/resources/00-taisei.pkgdir/shader/meson.build @@ -140,7 +140,7 @@ spvc_vert_args = [ '--stage', 'vert', ] -if host_machine.system() == 'emscripten' +if ['emscripten', 'nx'].contains(host_machine.system()) validate_glsl = 'true' transpile_glsl = true else diff --git a/resources/meson.build b/resources/meson.build index f03e0712..97e9919b 100644 --- a/resources/meson.build +++ b/resources/meson.build @@ -81,6 +81,7 @@ foreach pkg : packages pkg_path, '@OUTPUT@', '--depfile', '@DEPFILE@', + '--exclude', '**/meson.build', ], output : pkg_zip, depfile : '@0@.d'.format(pkg_zip), @@ -94,6 +95,31 @@ foreach pkg : packages endif endforeach +if host_machine.system() == 'nx' + # Package shaders that were transpiled + shader_pkg_zip = '01-es-shaders.zip' + shader_pkg_path = join_paths(shaders_build_dir, '..') + if package_data + bindist_deps += custom_target(shader_pkg_zip, + command : [pack_command, + shader_pkg_path, + '@OUTPUT@', + '--depfile', '@DEPFILE@', + '--exclude', '**/*.spv', + '--exclude', '**/meson.build', + ], + output : shader_pkg_zip, + depfile : '@0@.d'.format(shader_pkg_zip), + install : true, + install_dir : data_path, + ) + else + glob_result = run_command(glob_command, shaders_build_dir, '**/*.spv', '**/meson.build') + assert(glob_result.returncode() == 0, 'Glob script failed') + install_subdir(shaders_build_dir, install_dir : data_path, exclude_files : glob_result.stdout().split('\n')) + endif +endif + if host_machine.system() == 'emscripten' # First add some stuff that isn't sourced from resources/ diff --git a/scripts/pack.py b/scripts/pack.py index 5175f34c..b6236a16 100755 --- a/scripts/pack.py +++ b/scripts/pack.py @@ -24,7 +24,6 @@ from taiseilib.common import ( write_depfile, ) - def pack(args): nocompress_file = args.directory / '.nocompress' @@ -40,7 +39,7 @@ def pack(args): with ZipFile(str(args.output), 'w', ZIP_DEFLATED, **zkwargs) as zf: for path in sorted(args.directory.glob('**/*')): - if path.name[0] == '.' or path.name == 'meson.build': + if path.name[0] == '.' or any(path.match(x) for x in args.exclude): continue relpath = path.relative_to(args.directory) @@ -82,6 +81,12 @@ def main(args): help='the output archive path' ) + parser.add_argument('--exclude', + action='append', + default=[], + help='file exclusion pattern' + ) + add_common_args(parser, depfile=True) args = parser.parse_args(args[1:]) diff --git a/src/arch_switch.c b/src/arch_switch.c new file mode 100644 index 00000000..8ef5ce7a --- /dev/null +++ b/src/arch_switch.c @@ -0,0 +1,107 @@ +/* + * This software is licensed under the terms of the MIT-License + * See COPYING for further information. + * --- + * Copyright (c) 2019, p-sam . + */ + +#include "taisei.h" + +#include "arch_switch.h" + +#include +#include +#include +#include + +#define NX_LOG_FMT(fmt, ...) tsfprintf(stdout, "[NX] " fmt "\n", ##__VA_ARGS__) +#define NX_LOG(str) NX_LOG_FMT("%s", str) +#define NX_SETENV(name, val) NX_LOG_FMT("Setting env var %s to %s", name, val);env_set_string(name, val, true) + +static nxAtExitFn g_nxAtExitFn = NULL; +static char g_programDir[FS_MAX_PATH] = {0}; +static AppletHookCookie g_hookCookie; + +static void onAppletHook(AppletHookType hook, void *param) { + switch (hook) { + case AppletHookType_OnExitRequest: + NX_LOG("Got AppletHook OnExitRequest, exiting.\n"); + taisei_quit(); + break; + + default: + break; + } +} + +attr_used +void userAppInit(void) { + socketInitializeDefault(); + appletLockExit(); + appletHook(&g_hookCookie, onAppletHook, NULL); + +#ifdef DEBUG + dup2(1, 2); + NX_LOG("stderr -> stdout"); + nxlinkStdio(); + NX_LOG("nxlink enabled"); + NX_SETENV("TAISEI_NOASYNC", "1"); +#endif + + appletInitializeGamePlayRecording(); + appletSetGamePlayRecordingState(1); + + getcwd(g_programDir, FS_MAX_PATH); + +#if defined(DEBUG) && defined(TAISEI_BUILDCONF_DEBUG_OPENGL) + // enable Mesa logging: + NX_SETENV("EGL_LOG_LEVEL", "debug"); + NX_SETENV("MESA_VERBOSE", "all"); + NX_SETENV("MESA_DEBUG", "1"); + NX_SETENV("NOUVEAU_MESA_DEBUG", "1"); + + // enable shader debugging in Nouveau: + NX_SETENV("NV50_PROG_OPTIMIZE", "0"); + NX_SETENV("NV50_PROG_DEBUG", "1"); + NX_SETENV("NV50_PROG_CHIPSET", "0x120"); +#else + // disable error checking and save CPU time + NX_SETENV("MESA_NO_ERROR", "1"); +#endif +} + +attr_used +void userAppExit(void) { + if(g_nxAtExitFn != NULL) { + NX_LOG("calling exit callback"); + g_nxAtExitFn(); + g_nxAtExitFn = NULL; + } + socketExit(); + appletUnlockExit(); +} + +int nxAtExit(nxAtExitFn fn) { + if(g_nxAtExitFn == NULL) { + NX_LOG("got exit callback"); + g_nxAtExitFn = fn; + return 0; + } + return -1; +} + +void __attribute__((weak)) noreturn __libnx_exit(int rc); + +void noreturn nxExit(int rc) { + __libnx_exit(rc); +} + +void noreturn nxAbort(void) { + /* Using abort would not give us correct offsets in crash reports, + * nor code region name, so we use __builtin_trap instead */ + __builtin_trap(); +} + +const char* nxGetProgramDir(void) { + return g_programDir; +} diff --git a/src/arch_switch.h b/src/arch_switch.h new file mode 100644 index 00000000..ca1235b5 --- /dev/null +++ b/src/arch_switch.h @@ -0,0 +1,22 @@ +/* + * This software is licensed under the terms of the MIT-License + * See COPYING for further information. + * --- + * Copyright (c) 2019, p-sam . + */ + +#ifndef IGUARD_arch_switch_h +#define IGUARD_arch_switch_h + +#include "taisei.h" + +typedef void (*nxAtExitFn)(void); + +void userAppInit(void); +void userAppExit(void); +int nxAtExit(nxAtExitFn fn); +void noreturn nxExit(int rc); +void noreturn nxAbort(void); +const char* nxGetProgramDir(void); + +#endif // IGUARD_arch_switch_h diff --git a/src/config.c b/src/config.c index 474fb26a..33a2c4f8 100644 --- a/src/config.c +++ b/src/config.c @@ -476,4 +476,9 @@ void config_load(void) { // set config version to the latest config_set_int(CONFIG_VERSION, sizeof(config_upgrades) / sizeof(ConfigUpgradeFunc)); + +#ifdef __SWITCH__ + config_set_int(CONFIG_GAMEPAD_ENABLED, true); + config_set_str(CONFIG_GAMEPAD_DEVICE, "any"); +#endif } diff --git a/src/gamepad.h b/src/gamepad.h index fd483077..f0b53243 100644 --- a/src/gamepad.h +++ b/src/gamepad.h @@ -44,10 +44,17 @@ typedef enum GamepadEmulatedButton { typedef enum GamepadButton { // must match SDL_GameControllerButton GAMEPAD_BUTTON_INVALID = -1, +#ifdef __SWITCH__ + GAMEPAD_BUTTON_B, + GAMEPAD_BUTTON_A, + GAMEPAD_BUTTON_Y, + GAMEPAD_BUTTON_X, +#else GAMEPAD_BUTTON_A, GAMEPAD_BUTTON_B, GAMEPAD_BUTTON_X, GAMEPAD_BUTTON_Y, +#endif GAMEPAD_BUTTON_BACK, GAMEPAD_BUTTON_GUIDE, GAMEPAD_BUTTON_START, diff --git a/src/global.h b/src/global.h index 70fcaa6e..d12536e1 100644 --- a/src/global.h +++ b/src/global.h @@ -50,8 +50,13 @@ enum { // defaults +#ifdef __SWITCH__ + RESX = 1280, + RESY = 720, +#else RESX = 800, RESY = 600, +#endif VIEWPORT_X = 40, VIEWPORT_Y = 20, diff --git a/src/log.c b/src/log.c index 667e19a9..90f260a9 100644 --- a/src/log.c +++ b/src/log.c @@ -91,8 +91,9 @@ noreturn static void log_abort(const char *msg) { } #endif - // abort() doesn't clean up, but it lets us get a backtrace, which is more useful log_shutdown(); + + // abort() doesn't clean up, but it lets us get a backtrace, which is more useful abort(); } diff --git a/src/menu/options.c b/src/menu/options.c index 49a0d58d..371df990 100644 --- a/src/menu/options.c +++ b/src/menu/options.c @@ -164,6 +164,7 @@ static int bind_gpdev_set(OptionBinding *b, int v) { return b->selected; } +#ifndef __SWITCH__ // BT_GamepadDevice: dynamic device list static OptionBinding* bind_gpdevice(int cfgentry) { OptionBinding *bind = bind_new(); @@ -191,6 +192,7 @@ static OptionBinding* bind_stroption(ConfigIndex cfgentry) { return bind; } +#endif // BT_Resolution: super-special binding type for the resolution setting static void bind_resolution_update(OptionBinding *bind) { @@ -590,9 +592,11 @@ static void bind_setvaluerange_fancy(OptionBinding *b, int ma) { } } +#ifndef __SWITCH__ static bool gamepad_enabled_depencence(void) { return config_get_int(CONFIG_GAMEPAD_ENABLED); } +#endif static MenuData* create_options_menu_gamepad_controls(MenuData *parent) { MenuData *m = create_options_menu_base("Gamepad Controls"); @@ -662,6 +666,7 @@ static MenuData* create_options_menu_gamepad(MenuData *parent) { OptionBinding *b; +#ifndef __SWITCH__ add_menu_entry(m, "Enable Gamepad/Joystick support", do_nothing, b = bind_option(CONFIG_GAMEPAD_ENABLED, bind_common_onoff_get, bind_common_onoff_set) ); bind_onoff(b); @@ -669,6 +674,7 @@ static MenuData* create_options_menu_gamepad(MenuData *parent) { add_menu_entry(m, "Device", do_nothing, b = bind_gpdevice(CONFIG_GAMEPAD_DEVICE) ); b->dependence = gamepad_enabled_depencence; +#endif add_menu_separator(m); add_menu_entry(m, "Customize controls…", enter_options_menu_gamepad_controls, NULL); @@ -811,11 +817,13 @@ MenuData* create_options_menu(void) { MenuData *m = create_options_menu_base("Options"); OptionBinding *b; +#ifndef __SWITCH__ add_menu_entry(m, "Player name", do_nothing, b = bind_stroption(CONFIG_PLAYERNAME) ); add_menu_separator(m); +#endif add_menu_entry(m, "Save replays", do_nothing, b = bind_option(CONFIG_SAVE_RPY, bind_common_onoffplus_get, bind_common_onoffplus_set) diff --git a/src/meson.build b/src/meson.build index 47799b08..a7ab3dc7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -97,6 +97,12 @@ else ) endif +if host_machine.system() == 'nx' + taisei_src += files( + 'arch_switch.c', + ) +endif + sse42_src = [] subdir('audio') @@ -242,6 +248,53 @@ if host_machine.system() == 'emscripten' install : true, install_dir : bindir, ) +elif host_machine.system() == 'nx' + taisei_elf_name = '@0@.elf'.format(taisei_basename) + taisei_elf = executable(taisei_elf_name, taisei_src, version_deps, + dependencies : taisei_deps, + c_args : taisei_c_args, + c_pch : 'pch/taisei_pch.h', + install : true, + install_dir : bindir, + override_options: ['strip=false'], + ) + bindist_deps += taisei_elf + + taisei_nacp_name = '@0@.nacp'.format(taisei_basename) + taisei_nacp = custom_target(taisei_nacp_name, + command : [ + find_program('nacptool'), + '--create', + nx_app_title, + nx_app_author, + taisei_version_string, + '@OUTPUT@', + ], + build_by_default : true, + install : false, + output : taisei_nacp_name, + ) + + taisei_nro_name = '@0@.nro'.format(taisei_basename) + taisei_nro = custom_target(taisei_nro_name, + # NOTE: Unfortunately we can't just put 'taisei_elf' directly into the command array. + # Meson then makes an invalid assumption that we are going to execute it ("use as a generator"), + # and aborts because there's no exe wrapper in the cross file (which wouldn't make sense to have). + + command : [ + find_program('elf2nro'), + taisei_elf.full_path(), # workaround for the above issue + '@OUTPUT@', + '--nacp=@0@'.format(taisei_nacp.full_path()), # if we could pass the path in a standalone argument, we could have meson generate an implicit dependency here... + '--icon=@0@'.format(nx_icon_path), + ], + build_by_default : true, + depends : [taisei_elf, taisei_nacp], + install : true, + install_dir : bindir, + output : taisei_nro_name, + ) + bindist_deps += taisei_nro else taisei = executable(taisei_basename, taisei_src, version_deps, dependencies : taisei_deps, diff --git a/src/renderer/meson.build b/src/renderer/meson.build index 6e0cf05c..4a5e284c 100644 --- a/src/renderer/meson.build +++ b/src/renderer/meson.build @@ -1,8 +1,22 @@ +modules = [ + 'gl33', + 'gles20', + 'gles30', + 'null', +] -default_backend = get_option('r_default') +if host_machine.system() == 'nx' + has_forced_renderer = true + forced_renderer = 'gles30' +else + has_forced_renderer = false + forced_renderer = '' +endif -if not get_option('r_@0@'.format(default_backend)) - error('Default renderer \'@0@\' is not enabled. Enable it with -Dr_@0@=true, or set r_default to something else.'.format(default_backend)) +default_renderer = has_forced_renderer ? forced_renderer : get_option('r_default') + +if not has_forced_renderer and not get_option('r_@0@'.format(default_renderer)) + error('Default renderer \'@0@\' is not enabled. Enable it with -Dr_@0@=true, or set r_default to something else.'.format(default_renderer)) endif renderer_src = files( @@ -21,19 +35,13 @@ subdir('glescommon') subdir('gles20') subdir('gles30') -modules = [ - 'gl33', - 'gles20', - 'gles30', - 'null', -] - included_deps = [] needed_deps = ['common'] r_macro = [] foreach m : modules - if get_option('r_@0@'.format(m)) + should_include = has_forced_renderer ? m == forced_renderer : get_option('r_@0@'.format(m)) + if should_include renderer_src += get_variable('r_@0@_src'.format(m)) r_macro += ['R(@0@)'.format(m)] enabled_renderers += [m] @@ -53,4 +61,4 @@ endforeach r_macro = ' '.join(r_macro) config.set('TAISEI_BUILDCONF_RENDERER_BACKENDS', r_macro) -config.set_quoted('TAISEI_BUILDCONF_RENDERER_DEFAULT', default_backend) +config.set_quoted('TAISEI_BUILDCONF_RENDERER_DEFAULT', default_renderer) diff --git a/src/util/compat.h b/src/util/compat.h index de64dde1..56cc1b28 100644 --- a/src/util/compat.h +++ b/src/util/compat.h @@ -278,4 +278,11 @@ typedef complex max_align_t; #define CASTPTR_ASSUME_ALIGNED(expr, type) ((type*)ASSUME_ALIGNED((expr), alignof(type))) +#ifdef __SWITCH__ + #include "../arch_switch.h" + #define atexit nxAtExit + #define exit nxExit + #define abort nxAbort +#endif + #endif // IGUARD_util_compat_h diff --git a/src/vfs/meson.build b/src/vfs/meson.build index 948a70f3..462d815b 100644 --- a/src/vfs/meson.build +++ b/src/vfs/meson.build @@ -37,6 +37,11 @@ if host_machine.system() == 'emscripten' 'setup_emscripten.c', 'sync_emscripten.c', ) +elif host_machine.system() == 'nx' + vfs_src += files( + 'setup_switch.c', + 'sync_noop.c', + ) else vfs_src += files( 'setup_generic.c', diff --git a/src/vfs/setup.h b/src/vfs/setup.h index 6fdb6f86..bd73dbcb 100644 --- a/src/vfs/setup.h +++ b/src/vfs/setup.h @@ -4,6 +4,7 @@ * --- * Copyright (c) 2011-2019, Lukas Weber . * Copyright (c) 2012-2019, Andrei Alexeyev . + * Copyright (c) 2019, p-sam . */ #ifndef IGUARD_vfs_setup_h @@ -12,7 +13,50 @@ #include "taisei.h" #include "public.h" +#include "loadpacks.h" + +typedef struct VfsSetupFixedPaths { + const char* res_path; + const char* storage_path; + const char* cache_path; +} VfsSetupFixedPaths; void vfs_setup(CallChain onready); +static inline void vfs_setup_fixedpaths_onsync(CallChainResult ccr, VfsSetupFixedPaths* paths) { + assume(paths != NULL); + assume(paths->res_path != NULL); + assume(paths->storage_path != NULL); + assume(paths->cache_path != NULL); + + log_info("Resource path: %s", paths->res_path); + log_info("Storage path: %s", paths->storage_path); + log_info("Cache path: %s", paths->cache_path); + + vfs_create_union_mountpoint("/res"); + + if(!vfs_mount_syspath("/res-dir", paths->res_path, VFS_SYSPATH_MOUNT_READONLY)) { + log_fatal("Failed to mount '%s': %s", paths->res_path, vfs_get_error()); + } + + if(!vfs_mount_syspath("/storage", paths->storage_path, VFS_SYSPATH_MOUNT_MKDIR)) { + log_fatal("Failed to mount '%s': %s", paths->storage_path, vfs_get_error()); + } + + if(!vfs_mount_syspath("/cache", paths->cache_path, VFS_SYSPATH_MOUNT_MKDIR)) { + log_fatal("Failed to mount '%s': %s", paths->cache_path, vfs_get_error()); + } + + vfs_load_packages("/res-dir", "/res"); + vfs_mount_alias("/res", "/res-dir"); + vfs_unmount("/res-dir"); + + vfs_mkdir_required("storage/replays"); + vfs_mkdir_required("storage/screenshots"); + + CallChain *next = ccr.ctx; + run_call_chain(next, NULL); + free(next); +} + #endif // IGUARD_vfs_setup_h diff --git a/src/vfs/setup_emscripten.c b/src/vfs/setup_emscripten.c index 41d96246..f09d3aac 100644 --- a/src/vfs/setup_emscripten.c +++ b/src/vfs/setup_emscripten.c @@ -4,6 +4,7 @@ * --- * Copyright (c) 2011-2019, Lukas Weber . * Copyright (c) 2012-2019, Andrei Alexeyev . + * Copyright (c) 2019, p-sam . */ #include "taisei.h" @@ -13,32 +14,13 @@ #include "util.h" static void vfs_setup_onsync(CallChainResult ccr) { - const char *res_path = "/" TAISEI_BUILDCONF_DATA_PATH; - const char *storage_path = "/persistent/storage"; - const char *cache_path = "/persistent/cache"; + VfsSetupFixedPaths paths = { + .res_path = "/" TAISEI_BUILDCONF_DATA_PATH, + .storage_path = "/persistent/storage", + .cache_path ="/persistent/cache", + }; - log_info("Resource path: %s", res_path); - log_info("Storage path: %s", storage_path); - log_info("Cache path: %s", cache_path); - - if(!vfs_mount_syspath("/res", res_path, VFS_SYSPATH_MOUNT_READONLY)) { - log_fatal("Failed to mount '%s': %s", res_path, vfs_get_error()); - } - - if(!vfs_mount_syspath("/storage", storage_path, VFS_SYSPATH_MOUNT_MKDIR)) { - log_fatal("Failed to mount '%s': %s", storage_path, vfs_get_error()); - } - - if(!vfs_mount_syspath("/cache", cache_path, VFS_SYSPATH_MOUNT_MKDIR)) { - log_fatal("Failed to mount '%s': %s", cache_path, vfs_get_error()); - } - - vfs_mkdir_required("storage/replays"); - vfs_mkdir_required("storage/screenshots"); - - CallChain *next = ccr.ctx; - run_call_chain(next, NULL); - free(next); + vfs_setup_fixedpaths_onsync(ccr, &paths); } void vfs_setup(CallChain next) { diff --git a/src/vfs/setup_switch.c b/src/vfs/setup_switch.c new file mode 100644 index 00000000..e47596d1 --- /dev/null +++ b/src/vfs/setup_switch.c @@ -0,0 +1,39 @@ +/* + * This software is licensed under the terms of the MIT-License + * See COPYING for further information. + * --- + * Copyright (c) 2011-2019, Lukas Weber . + * Copyright (c) 2012-2019, Andrei Alexeyev . + * Copyright (c) 2019, p-sam . + */ + +#include "taisei.h" + +#include "public.h" +#include "setup.h" +#include "util.h" + +static void vfs_setup_onsync(CallChainResult ccr) { + const char *program_dir = nxGetProgramDir(); + char *res_path = strfmt("%s/%s", program_dir, TAISEI_BUILDCONF_DATA_PATH); + char *storage_path = strfmt("%s/storage", program_dir); + char *cache_path = strfmt("%s/cache", program_dir); + + VfsSetupFixedPaths paths = { + .res_path = res_path, + .storage_path = storage_path, + .cache_path = cache_path, + }; + + vfs_setup_fixedpaths_onsync(ccr, &paths); + + free(res_path); + free(storage_path); + free(cache_path); +} + +void vfs_setup(CallChain next) { + vfs_init(); + CallChain *cc = memdup(&next, sizeof(next)); + vfs_sync(VFS_SYNC_LOAD, CALLCHAIN(vfs_setup_onsync, cc)); +} diff --git a/src/video.c b/src/video.c index 2b8eb286..984e5e0e 100644 --- a/src/video.c +++ b/src/video.c @@ -66,6 +66,7 @@ static VideoCapabilityState video_query_capability_alwaysfullscreen(VideoCapabil UNREACHABLE; } +#ifndef __SWITCH__ static VideoCapabilityState video_query_capability_webcanvas(VideoCapability cap) { switch(cap) { case VIDEO_CAP_EXTERNAL_RESIZE: @@ -78,6 +79,7 @@ static VideoCapabilityState video_query_capability_webcanvas(VideoCapability cap return video_query_capability_generic(cap); } } +#endif static void video_add_mode(int width, int height) { if(video.modes) { @@ -622,6 +624,10 @@ void video_init(void) { const char *driver = SDL_GetCurrentVideoDriver(); log_info("Using driver '%s'", driver); +#ifdef __SWITCH__ + video.backend = VIDEO_BACKEND_SWITCH; + video_query_capability = video_query_capability_alwaysfullscreen; +#else video_query_capability = video_query_capability_generic; if(!strcmp(driver, "x11")) { @@ -642,11 +648,12 @@ void video_init(void) { } else { video.backend = VIDEO_BACKEND_OTHER; } +#endif r_init(); // Register all resolutions that are available in fullscreen - +#ifndef __SWITCH__ for(int s = 0; s < video_num_displays(); ++s) { log_info("Found display #%i: %s", s, video_display_name(s)); for(int i = 0; i < SDL_GetNumDisplayModes(s); ++i) { @@ -660,6 +667,7 @@ void video_init(void) { } } } +#endif if(!fullscreen_available) { log_warn("No available fullscreen modes"); @@ -669,6 +677,11 @@ void video_init(void) { // Then, add some common 4:3 modes for the windowed mode if they are not there yet. // This is required for some multihead setups. VideoMode common_modes[] = { +#ifdef __SWITCH__ + {640, 480}, + {1280, 720}, + {1920, 1080}, +#else {RESX, RESY}, {SCREEN_W, SCREEN_H}, @@ -679,7 +692,7 @@ void video_init(void) { {1152, 864}, {1400, 1050}, {1440, 1080}, - +#endif {0, 0}, }; diff --git a/src/video.h b/src/video.h index 696661af..bf0f3d3f 100644 --- a/src/video.h +++ b/src/video.h @@ -41,6 +41,7 @@ typedef enum VideoBackend { VIDEO_BACKEND_EMSCRIPTEN, VIDEO_BACKEND_KMSDRM, VIDEO_BACKEND_RPI, + VIDEO_BACKEND_SWITCH, } VideoBackend; typedef struct { diff --git a/switch/README.md b/switch/README.md new file mode 100644 index 00000000..d465b6ce --- /dev/null +++ b/switch/README.md @@ -0,0 +1,43 @@ +Taisei Switch Port +================== + +

+ +## Installation + +### Grabbing Binaries + +Download the latest release, +and extract the archive in the `/switch` folder on your SD Card. +Then, run the game from the [hbmenu](https://github.com/switchbrew/nx-hbmenu) +using [hbl](https://github.com/switchbrew/nx-hbloader). + +**WARNING:** This will crash if executed from an applet such as the Photo/Library applet, +be sure to launch it from hbmenu on top of the game of your choice, +which can be done by holding R over any installed title on latest Atmosphère, with default settings. + +### Build dependencies + +For building, you need the devkitA64 from devkitPro setup, along with switch portlibs and libnx. +Documentation to setup that can be found [here](https://switchbrew.org/wiki/Setting_up_Development_Environment). + +Other dependencies common to the main targets include: + + * meson >= 0.45.0 (build system; >=0.48.0 recommended) + * Python >= 3.5 + * ninja + * glslc + * spirv-cross + +### Compiling from source + +Run one of the following commands from the project root: + +``` +mkdir -p ./build/nx +./switch/crossfile.sh > ./build/nx/crossfile.txt +meson --cross-file="./build/nx/crossfile.txt" . ./build/nx +ninja -C ./build/nx +``` + +**Note:** You can optionally set a custom prefix and `ninja install` NRO and assets into that folder. \ No newline at end of file diff --git a/switch/crossfile.sh b/switch/crossfile.sh new file mode 100755 index 00000000..338c62e5 --- /dev/null +++ b/switch/crossfile.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -eo pipefail + +source "$DEVKITPRO/switchvars.sh" + +function meson_arg_list() { + while (( "$#" )); do + echo -n "'$1'"; + if [ $# -gt 1 ]; then + echo -n ","; + fi + shift + done +} + +function bin_path() { + which "${TOOL_PREFIX}$1" +} + +ADDITIONAL_LINK_FLAGS="-specs=$DEVKITPRO/libnx/switch.specs" + +cat <L4(fdY3pi(=;-J` z&wwA$2?k_;-`~X%1TruHT?K(aEFcCt0T3f_M+X!wI>GF1hr=6Dg=_MllNr!m&?(@dbaa2~Q-3c8`oA?J0|Pw+ z6C)GTzlE8Vg^8J!nTd&oorRU{?*-hQW@kJ7_m96{@*iDKG1AjBvN1C;|J&sMQ+3h` z;%1=(#z{|i5p;^1j-H$Dq!R=NM$Y)}@cs($??rcto`I2x85jmT(4hJZFnoG?V9<=f zh=JB2z~>+aZpO10Z`@|$dGwt5lGi!;cL~`nVt2~hc#Ve%;x}Ks4rOKI<3E2vKtfXL z@|CL!ib~2Vs%m%d-PhK6psQ!{_=&06Q*#SDdk04+XP1}WZ+v|H`~w2R-iJp-eu#=r zO!}0ZlKMF<{aa3MUVg#%!lH`Gs_L5By84Fpj?S*`p5DHnBco&E6O(_YrZHID;?nZU z>e@PSXLoP^;E;55{8z5Oa{lM_f0c_Hkn7Z69Weiui|&*kQ0TcC7%$#nI(z#O^K&nr zOY-kn&fQ7KE^lKMyJ<||eersjjZa(wBSHKt+P{+hzX=xl|4Xv}66}A;H4QpVPX{QE zo*M)K9ipPt_PbgTia4cJjPXCPd6DWJSSKtt(jd{nh2H1Lb)T)`z)Pod0$!D*VTR=^ z3U>O|UpQlRwMx9E+v6=_$@WIDKJni4i{5MY#@U~h2Sy2H8s1C!1#?VH8Y5ntj{aPC zYl!?EL%jaqrk5A+{imj;OFPi_#1o!;L155mWkZ92MWt!$p14v91Q%knJ}n57pV-0h zyG{J+N{$ITtf^o01IGoBqTk@OV?2gaXg0f4KE%!OoGadsbzjzyQ`djaOV>;VD9PCc zlukc&w0yPT7$9cPqOQjmd>u;lF8SLtMwX{-n^K0z&K`oCER_63WDdQ{OApzpZrA1UD z3378N)-ym>cn&o*mgw5AI+a)X`{K?5)hhL9225X|K?A{G>WT(sR;-ta?~DaVFN#0v zzA<(R@&q*Enc?6-LRp3$27V6Q?(&SZ5q69;A^K*OeNpf#Pm9Ei!4?$zT_M#*9sTm< zE?zIq>~a*m!fi4uf9#)tv|1dcRob3cEiZ?p9-V+Xgg?Ee+_!djSbkrN_T6f<`0uaaj>5!Ab&zZmP#*D`ut*E zZ`H)X>In#Wvy#g*1~ve*(Ux7VQfMx}oj+ZIvVXp;&njr0x*W`%iOb)5b*n3oIntvF z#X-_*7iND_8CU8-SxzW0K2(+|;xozO6L$j#BQFeiKP%E!6yGzDg|O31kJ2Y&>y5`3 zj;VUIvxHAtH;KiozvmPtj+b_F&CbdV_Ol7^y085JXE2BAo%SBmCvvq5KsqFveix+Z zW-IgIh0OTFsL~(>WUZL&APfC@h{fJz%(ov&omz3F_#A_cLuAu(rg@O_h;DeHqtI~&V_chZgk1LNWA@~l6-T#CJAXG z%)j}#X-G?L!Wli&qdhq2s{ZHJ;|~=L`%o3;8m(_FUZhg2ffmlHF^yg6p){SyV^x~a z9@u7o30DNG)Lvb09i{F%PmnmUa0ZtK$zlvjaGu@L`o%Bpii(Kof*B3W+n-rpU!KJZ z!JirBwoMh`3((!c4O7>X8KCoXl#*9N>+2=LRpr@>_k&;j!WjJSL=tV zH;@_PyCu)<5*_5PKZJy8brjfSzSj8%#!K439CnPs!ne0zw>*GvvWhF_p3_Z1J& z=WU!CgD*_dg#C52PeAX(EJht^zaL}_G|vj3y*^NV#z{=I59DK))Jh*tn!yd|cPmd$ z8aTc1Fntow?%Sbp=KH)I!z+-LLPg+@N49sPyNu%>v7JjnnnBB^p~|(cV!v{nHyLl`(6lY|AotmO@sEaD&H4|e4NonPpYOX~4{JGRX zf@9BLbVaIYOzbKRORlv4Ff_mAMwvSwG-2F{%@Poz)aYjzsM!o{zg$?f$kUnbPdW5h zApCOzI+L4j`lZ0)3XipC-6M}*-+4bhk6;z|5vjJs9Ja&G2H%?WbXNfL{h2WkPOmf& zhcC->!{r?5&+1q?5!S41pA<%QX@9c+gea|9b;sLj$7Go4tk?D|fM=~=P7deih+mqP zw}F^UZih%Oa!q@7njUA&kg_;_{>Ilf#XkQG0-55*I9sYWu&NI46I&XoL$+hPljL@C z%Wk)ZWk9a0fM4=g*V@x~4~K+nxVGs6(YNZav;*3Bmn=$K3iBm00v-3#K!SL1Wjcb39Sf8{t8&I$Zp@*sU`QYr*i4qvc;cW?(m6p`fRr%zjh;ycGWS4;go<(7u!qIv zQuC#sCP?Fc(*q4BtPWlmEBr=3$Deb7EZG(>;8FQYQ+EYiDEwi~#?0HtJ3!sq#!6Qv zQOQR4eC3UO@qJ{>uS$Wma92EU1+4*b`+a*4yWyP8W_X!D*!)t!>gV=4a_V zEb=bEJ>O82nE$2nZna%Q)a%C9!RtD6!Fwyr6J;AGAWjNLX$I_qztdxXH@WQV+9~DV z6X(&wKNBOKg}nA29N*}=cX_z;(W|T^3rYdTGf3Qk#OVi%kduQ?1S@MIfedN29UO52MvVtu8&Ss>6W-zipu!iQ;Q+9TG-+* zU+l?OeLF@*Yy0pdTKapWY2YEP#kMe`v~wEu;mrw%X?mtZDYMY3N22*lw87k`V7maS z*V<%t&^=!fdO}3TP21Z_8LukDU@p_!zj&@6=@t2*g+RBrq(}PgZV%?^72p5n>XSEF zY&JO{R9EHYIbQ&d^(u_Z3Db55=+P_wIKqTltq3$Z-fA?f9>FsXjGMG^G_v2>!f z)`BjPGeLd5;c-Io{itgxci(|!dm(e0ucaGV*`CJUS+Oh)E^SmT-ZhDU7cN3o30ch; zO&5=`fQiH#>D@bH%sa4YD?;z>J{uY(Y+2+gF(S^LBlq}?mqGBn)8~{QG`!`jHqdTg zA7~HBC4U04fBl|j69h#hY6{71m*94JIszeGLudOrmc}=pJ~gN+s=`>8|Hcc2dXK%m zfwzgvUzNqU2uz5kT6}3z_{AO&Y#$%!HRhGjFJ#B_E-$LKx_HN?k&9bRc`#K+}(KFx5GD_tx{np*F0&i;@-KF&*gCu&? zypNmaS_|viY$GwMo!=1r!^&O4 z(x?S2txVV2s&S3q^E*Q#r;aWfPN!7x-C};D1suO{_sNWtTk(o!XS}K?qWL0C)yObR z$j(g@Qevu~Y9P2?I?+FnNAkOr+nlMsFj}xcN^)J`!qd*{pMY4O63-Sbtjk;qxDcCi zxhMJV;31lmDy`EA;cK}tfBX5jbuH;}ukglUF`;VFIgD#ha-i=@_sToK}Aj@)0)Lv9ae(lb8>}_oj@l??+FIOIKubyIgIyrfn-)3bj&{VnZ?&MXY^W zCTJ-6%<-*SOfLp*?i!=A^?B%k` z;oJE;)642gx*7oH^l1wVR`B9Y_WG(w!c9Q0Gqtc+5A4Y1@Hi(-?T}TXZlgYoGZ<# zcooT+?IoO&Ewwd1?&opw+seB-@17@`5jvx0*6$W|n-rC!?JYm#n&;M~YYr?+;#*_= zbTk9zvc9$KWM)e5S{%bt=L&aUj|ABkwvuw%)gV!uZ|0=#ObFGFZpn zb|}Ma+tKt}i+dwbDneGn%iC2c$MajC_^;GjoxUaqa*k{8=w%wAJ<#bwnUy5hv}v+l zU~W<^)KW9V6rmpIYRTPiplzW}3yEjeCS|yLf3sbi_JyCmsiE#cfyh@K`1L}K?ToW? zKKgXu?wy-xfHMqy^}pJ^-qob)H552mV%43g7YuUUNblPJN zEXVjm1BvtO@+H_LPWYO!9ak?uG2&PP1tJcOMDRUyPX&Jw!B5(M`b{hZ!3Z8Q!gB8B zI>+U^O%?qbr5Zao8_S&>Rb`BfU8;}z_HW+@oqI_4f2$0m&7UPZg=*_)pN=nJe6V_7 zkZw<^Z6OLq_~N3z^%3UsG+e(EuO3|4|ntGaAA#~T4cLEbn)m5XZE@9m1ZB)?y(M& zEBg zn9{fluU7g<IVp+cdQWm5-Rev!LL9Xm8SHvZ=E)Ssh}4w&nZta^lhK+M%mNCSg}JBfn>_ z#|>R5lKpnmbzQ}eyZ?$Y6a|IvX#|fX;}xkei%hdl80*x%VgKo_iRGS0?mIfyXP+TF z%tOZZnURx8HbFUSl~(h8B{p4(ixJv7 z?rltg{8}qh6n?p&T`{`v>Zb9kaa;ENJNrSt8$Cl+UuxtHZ^kt#Yyb8UmK?7Rxvyv7 z=O&`$dE^Km6(M`U9kr583sv(c+pG60YzgxZ#4ExK|rJAY~y+9XPPDVtT&k0{?*na^PQ4$>rhCZovB$5C0TgG0A%*y$6l_ zHL_&<#ZpaWOQG4wy!LlG?+$)BQ9r7pm6v{N<@Jx8xQ4JEVj>3i>$AzH;&#YHLC@r5ty=QtbogezZb3qB@(k(o zm!zYyCr6)h7q^CfSV)V0`^b2XP6<))QlNVJsf(}itkK2JR~;7|?KT-izyROu}Jj4M8U9APP500yzDI!rC67oZ&ok z!MfBU+GUb$tI3AN``YPmRyHsfTYd2|>zPA_uP>fvSt2ctM{u;$V}DJBqDpeOR!R&z z=q8qCj$L=$?&KM;b8(@!`)>9RBf!!kNKAo%f?|mfqI`^6yQ<7JZ&&5vPgCA+a zEpJb!AHv>42zoHm75<)pY};@|xm0JfF>4E+0_AG7e6*jI!-9(X%2i5Zg0mloy8wO?@gG?=znmk-wT@ zYbiocPCyJ*zyt-KfZEh4mY9w0KRvkp=B0z`5Cyf#4)f}9RQ-t9lb^o?y&ps&QqLyp ziZNh*&>h9x2k|O~?@y$KaOOB%iFXz^zvM&XV-_8a{o?S3%lg8;PP-gmSCM2Y@~Q4p zkNi1F-eYA(FE<5^hnv)~vRVc@_my?sMr1zrS2e_ya36`@xL^Vg4w_v1 ztoA!aKvAHf0TzLx`Lt^kpJ3EtjL3uYGUpIm-#&WH!cA<{ z$tI*Ou?W9Ykp!R6Wsjf1Oyq{QX^b{-BrA&;Tugd zS#6I7rF1Wv+jRI(5Pw}vw(5{yvSn6(u*DTf4WTBX&{#Q=6o32nskH{FZU{+C`K{fA zo2*#0yGdK%laKLBc{`F$ElqDGh_ierpKTt!bn16Cxll+mSA~c3rw!_ni3Zl ze)j3=L22R3S2K9mjQ8OW9$X6kXqz8^tV5x3a-^%UZa7I2vT2F&6oL*ZA3MO|8rIm) zGP0WshrT)#-Iq4LsJ(6jOcxW}D4m z0CdUOdjm1i;!bKK_8-KP5G5hSxYW*VK3D!$d*u(+R~A3gFC0cH(6y(?_}%FHpBs=G zSGb7$aEnN=-fxOG7R=y6lPDkYQStE9fD~VGb-$zV%;Exw#wR2|dk3+TBgy(v1Rr6C z*#7hrgAQZ}$Tq6S|>`CdG_Vzs&TWj^Mj)76rN?z?ivF=tcb@ zk~A~^K2~n3vB$P@AO3Euv`KBcHqzn8MAkN5_uM-tbLDUD&%{5J-9)6<+mh5!2P4!U zuxHzmS|H-#CW>tFGK(9=_cB!=WF=x#_J#R{OCKQ9Vi_VYc3x?FjS8>QA8nBuPe6>~ z)Rq>_X@dfaJ@tpyMgQn&+$C3clg`7l+N}rEmgkXt^-Yb?5oAZpPngrn;RIA_e6jYy-iGVih;X6M$bFBj_<=siV>Zp= zB}7zd5zpsJO&MgwKzdBF!w#QPmDeg7$YoO`h07F^TY zEBHY)EX$dGPmIPo2lV{pk2>(7mNwB7aX2}A07mmk6AxC}+MC85$7A4P-;g+cTgEP^ zH}6)$!K3(Fw-DD((`4W?7WQ0R*J(}c{-4hd8=MZN5)&1DU>o&qcD%MgMJU&}{e z#-?dvFD(l2Hwc~>Z!6^~1sLnP*#`fML!Q=p$a`?%S1xv-(Jck(Qd=6^F2~|&Q+2M0 z5Dg*wwscSn}`2kuwR!E_qR>ufCMcjmr% zoq*_`aXk5pPgZGXNpKoB?1P#|o)|uAc}EII5@%$k+(fjut~Chcv7Nk(uX2%(*m$G_22{~=2oOsAhmqXY*6F}K$N{{p(Dz5gBcfKE|hDN6#d-U zT1t~MzZu^Sm~>8D07QEWLl42G`f(GevS2-G^$Ey1#Jj6@L@`!Nh|u9*bi8bLsj+1D zS5^>DPfAn*6=wrXEu$%*?kj*9Sp&@3cLc#0`2iu)BtZbVK+o5WO&Ssao+7&1fAjYm zfg|AuvkaYjMgj35BPrzTemrLh${!VM9-LOIjTYWntJ_4l}Ue)DLYVOHqfzPC$NUOD^t!Cbb`6W2}&6R zi=hY2&U!jn14vJ5#IceX_1W3kCW+=)hmhhUz}-S;fbmMb@gzw|;py}I`P(2uz@8=O z@6=_Q%AYROr=d;j1M(H5b7OIkChH1NHsKGw%U7=G)vu?TEe|xQ(ZI|M&GcYP78p^E zT&I*jH0gNe3>4`Wl(yf44)9!Rbm(Q+N&pG#KEHJVCW3MRPv1STCC(yF(;&h8M6Ph3 z-fn1Hu?tdAZSie?_XR^0u}c{Q)ia+)9{x&m7x#OUrZA`9u&oY1xq1RaZLB@W|t?w#lCP&CH z(6$$FfMA*`tj!$dmdT9Mjv@kC2T)c*ehlXPo9Jr+jqXbdE$ zw!mxI-xuyXITTq?R33DCZ1Vb9HF!P|kNtw1c&k-T1&j_Ie**fUOgWc>*>FMd)6Tfm zG*qS34^vz)b!SH(RnMZNS-HKg?q*gLBfm1I(+q(gg@7Fy!Opl+b9R(Jko)YP!ZF!reB6Fy#i;b6Ou$IvK0M z?oucX1Z`3*KBaiu%;=5lNfS~Ka^3)nYu%U%81ea`+I|ey5V=$(8 z(F1t`Vp;eQ%p6c`E%SNkQ#VmO_M`u-rOSSi&3Ql_87Rj%eQbUt0Zd~}0KjVo;TQ-V z3sea>MVcP!SN<^Hl6aNyjyIhNlnD9o;w=OSk)o?$fbX9J<_?9-^K9YzPGP#@3g88L4HkkdXlcKP+PCx>|23SNSgg5WZ6sf<{MyfuM zd}~aI!IU2^m2u7>eB~8OhnOHj3xI}U7zfKA)#O76Gp(P?CjDOL z`Nyp@@``6b9}DHJ5XQDv6-2*VOFPRE1WTh<`!tdloPijbTCq&UkeI$g?B;CcP5X3^ z`#F$V1*{En0&?{fgm%DcnBT!%5U18FEe0^nuldK$Dz(;EcvoQggjt-^BDn5`jnUr% zPaVQ3cL`vu-{yt`+0efh}J zMnjW)m$a>y+6p~Sa7%wbZIg-#(zhp@44r!<_a)(|o^GmUyN%8Ji`g=S!mmzaDdrH1R4QS7|C z4o*+I1Fv3TDN#5%;(Lk>`vtO5y;#&Y}0VTba zJ{AFd#|SXbxIz660(1l~d#p>`<$X*k`h9l;pV_Wi-P79?DDk=#RRS z(LOITu5t5KU6SFLWA&?`^NW{Z$sp(stnIwd06&ct*e)5kNTtB+3K2JS?6B}C$EU$u zo{)Zp9Y$A6;jrQ>X%rm%$T7T8fWkYuUwi^O>zbN%aa=;)YQb99%S%is#qUKY&G9Y7 z1yJoz__SDu78CV5O&U5lP0Ks23{@e_BSLb-M;Ld-%M;NLVzNq1*9_G#TzP(Ag4qd( zq$qdroisqUBUK*6Qls{bIA7X;i4&zt<=>st^Yae$GuYSu^m)him*WETRW4R2qvPdo zX-Sh(fjKURwY`7}`M)H0-9>9B70pOP`KqX!D6;uW_imUvD_3pkj~z2C|wf4_?&CVS@T!!^U4mFC6|hqDeMeC7*z5k=kC4 z;H{xWGitGLQ>H|a!Xfhs=r*t<6C2EXD6vvIl#c}PR{0U_`TYQ=XAiSK;~ypZgZ#d7 z{n;id7t=UqiD3Lqq{~80ExrenreGA_pkKNYS;e6nT&Qtx)i0nTq9^s`jj>%Rx6Az@ zfZ~{$Y4fNWsn`>cqY@Q?LVrepG#-^uRL4v5e-M?sAk0%yc!7CIXNeybXKsWPJ<17C zde`>2_vi!ju7isY&L~1lv3C5dI$qFv*oS57;!SPBti|j-O!ALb`HG6Lbr$4CMzgn0gpSz&5UA+44cPYop~eB_qDg-jN2U*HFSur)6?3eJIev1BS-T$i=Hc zQ^`!UnR%rL6bC1PX%e{G;(eQmRh%~8gspjq$A*JfaTC88U4_QiM{-tC4<@&)_u5R$m4DECusUF?cL_}^f6N@Ih zOs@(&porlZ$+A0_X)IeNZ@kM-KrSyt_Kz6J`p~Y0YNO2nY<<%OvVJf-k?XzZ^N;y> z_@PVXNH;y`?K(`E>z4zJxBetg8@&pX74bmb`xztp*KuscwxaOKdUaJ=iKJFR4 ze&aaFHWF0@+uXpYz|dv$8q`cJF%ovDT}wC#7g1;j!wTQMB&=>2VraV;0eW-hkxt^7wM`|B+4Dk@9%x!K>duV8-bppNL0Xe34EVkM?T!K+qu72y#6@&?M~6`jzRFn zTf$Q9;8-w^)~R6akrr8s2r=_8k^)5WWxG?0T}C!jSeai1!5_Y6*|5#F`HM;`A?P18 zu3_AWr`9Esj+OIoxfw+yyJs4;FZwY9?HpY@Z4^v+9ZBKMILIMz<`DY35W;SI!u)2{ zLGFf=?SXnz)t%R_6Vuq$-`*BQ$8)&A+J3;8wzTA7ZT2+5^_x=w@%J7x=7YZ_zeQ&} zjxmMkI>oL`o})X)0Y?8N*ekO2FCz71qMQqRxKl=P$8{lonjYoWR=iJR9`)(8&4bA4Mnoiv%?`eqs`egSKUV$y*31&+i{G~0D9T$YwYgHFD+y6IB@yd% zxkuxQ#CQ4zKz%?s-Nln6WrhzU!Z4|LRx$^v^8Q%0yK9FxLg*UY@nVAgd<0kZWqO|Q z*#ZspYa`y{8)WZUBP6UX+k!LqJO#x`Q5)LhC&-Z=GB1NEJV7+JIkFTmmP-&vOC%4# z6EhYNC-K}k^#HL+jVIV*n3MrwuI|O9>e~BS_GqHm5Bu3p`j4S45NZVk{S{HYf+ymp zT107M2th^IS;Ntr7HcC0OHZfd>?7u{i``;k^4(JD6W_fh0x4MMKQ^GKd*H336!p-9px1#ZtOiCm#>0Lt&Tv^^c#K_e$buGz}dreOpNThiqr z$~pShIN&gvMItn;L|E*oCl8AemJyx;dXZP#AE3>(x!g&btMp5Om+6n0fJqji=uN75 z;u$eKQvEJckz2`{8Vj}8U^~1}=wD4E7`C9?z>R&2ltf%uj2!p1jFLYU*+&%e`O^io z6Hq`7;VW0Ddy^d2b%?Y_!0(~<2M1=3*!I8?h;lQ$H}T_lk~vw4G-|VOXlX#8?9h|K zzjA%0@xj`f!Gx!fn6o<6Wb!#uj$X7;qZjt_M7KuZ@*Ne5^qOZlir~ag;id29?(rtF zcOkea(n4R7lgxwa&jpn@Glf*%65$8dM?jxpz9`QjlH@{oBtYM;nnD!_FTdFMC$;N} z7uyZe)y2jQUy-7#ztE#Gxhh4uphK2izu^kwgSa8jI1ionnwlZm-mXiL*BBO~ep@|# z0(#ToegX=4$Q%R6y#lT(fpEh-5Dc`zbb}L6rVAR(6jR*ELCZfraFUnQ4F2psLu0g! z+xwf!6a?8UsvE6#;UZeyLFK^W0@IW!u@`qKX+7{0kWXs=t_*F|G)^jXbE_50@w6;N zStb&AMSvHxZ$46^dMwxb1x1|-{P8!h;8^<{Z`DERQB zHtR}KdySbz?n4~Y`7)=d`O8DbMLMD)-ZKOzXCDvS0mi6ct6B2ZM=Pv$02Osr{KSRt7 zP21f|-=>}32%XE(azM3fGCi@)FLkA|e&rf8B8f$IqDU8q@TvsFeJU_v=v-9gU@``F zzD?nWKMY%4lYK2q@uo~(!nLxCZ=|pCPnTZ2{N%A$EciRHyQ4_*>p$Huc?v*VQB}dI z|NK5WXb~iqwO5{!FXkNs(s%3_+eG&0NCo7lfV5Sqn-HSK3L1siBp{6mWzC+u;{3zV ztz`|Oj67GO^N`b>+xy5H{}Hg{8iSA&!3W0#5b2spUPQ~zhih7bq|)4u+?s43 z(Ff1RE+wRo_4b6DEdQ+6aOi^e!rB}G5%M7b&HslU7as|b1*r0QI8m@?dLhE(;pjFB z!#cWMcDC0*=0TkItME@AafJF-X254LaYi&w5*1DXEkT@+B#D8D^l~EooMK#=CZ{Y8 zTL-`ySQ~z!TBBr`1U^tAFqVYyko6xmCwzjnia$Dj$jns=Yg>g79`*0^=#u0JgB_#p zalbyFfLij(YylcDtOFQWvEK=3hpUFb0jGKqMp_`-6#!RlOXC3e+|WJBU$PKExks$s ztR%ne`#SF*Zy;OEbvnjcQrh3MWX!$3+IfsIA+&~a=5_XND{>r2dLrG$FmJ9AusbuY z^X$Ky#4wp6^H|z)|Gm-Y71i&a==V$ZCO>|an-o9$o0$k8A4V1G_iaFN+cPs*I3Dch z?-VY-CsQVrH7>&Z18eet7i+|w0EmgS6gT_D zpQGZdYF5jeQt#ChP<7WKe7&#Vc1vKFdghVM z#Z^WSdgUA)71-z6E~3y&MkEQNHh7qTQ9C>iRvC_Jqx#+djjm}TGDlLBu5Io|UHl;R z*`VS=X9cf}pX6`L?R{hf1Pw$A39ov3AfzkXG1pqyf+bp-2s&-Oo=wH>(t|gKN-n#} zcCGzGX+D3Cm3=D^H*)};7z27&_K!KpC*8ntFn7wLsh9mT+GsKrKC)-q?^?~2tGsc1 z{~$uLgLx()xeIoA>^~AaSEbUY9qMY6a}t&QO_te`{zl3q!m+j8r7tEsA^cjGTt&oN zDt|eGgbuzwHQ+m9 z(DUk-+0oU>`j6eDdaYqtx(4N^ZM}Y@_HuTf4i?N(lO}m;UBK;NLHFE`p+#L}@?sn| z;ah@lMa=~xJ4>fw+POyL4y-WCTrMsE$YlFCExls&@ZY>KsuJLIP#C&dL<5*#kKiYx zG=|_e6I=V9p|ndaPd%Tf+!Z6eHgjXIW2WpfEI;TWEF#625A-XJ-quHTx=UNkeXnKYhiCnYQ|If?O5Rg2Kte@e-DxWEhX~JqdtY=)4KI(p+)hF zlM|*ES4mE{hJ$bLc#_p_vkt2M(enJ24Ix}THUvi!9adWoft33lZ{1+_acXDk>zd>u zn>A0*8kMc?9LJnY!cV4GA=e?NcWqTDu7p1XRA;8%poG>xJM5oXw<C zG$(9xmCYIARw`yA2BpSp79p8?jfEsfOIFBPXmzo;`E4UGFa=GAmRO`tyX&8ZA3L}6R*Sj5ufg|Ij^O^r50-R)&5*Wc~@Pj9l?3bmqJiHLD5iPL`%{ zO*Y-YE#Mn**rRI18LN4&@(&pD^*cR3)jtnp7M*vyur9q42+A-J2pDtkE_1ZBRfUeZ zx9&0o2-d8`{TaO3?K%hXz{?%*i~#YD9x~^Kjbw9WQrm$ z;8gm}96Ef9F)(t|1m^1?IaGvu}?a# zqXj^1)UW%t?*$gRk!RNz=oC2a>Jvh`kvCVB1addTf6t_<-xlS(@!N+UloUHDcnb_{ zWNmy^;Bdql7&;rpMPsKpk?v-bdOBK^CbCBq*)?BFJ@<%5AC zH86q!z#0&YjN-D+jA=2EvUVSmU_?k;O3P(JURkN+#+=%PcLP-g#qQ_`jgNm+Mbk-@ zuI_WSu{Z_Lr!zW_Zi#e}=_wCL)`XmcQUW|q>pF!4%&njva!+QSe@=-U;c@QUz1CiI z)|n;Ub!j*?TeLb72ak3Kf@1GY{5e(j``^RcYB^Ms9(2ABNu%u8pKfNc`{@7mv?mWm zjd&PZ>W*xBjvg@4k0AP4=zo9Oq<_8pb>7=KlIgwc{a#nw7~FOqv^{!kU#<&bJlC0PMAb5$kvrR8HYc>w4=%H{U4n9+hAkRJdp|e_ygu&(@ zyX#}zqLUw*GIpl1&D@csNni2D{;blt)?J7-;4K)cjIwtBo| zfMcMEnCzcxd{>OVS2UDGljXZJ{Vd7>!N#F{XG}}frY-EIV34_B?A4u4DCC$i(CJ-C zhRtww$ZK+c{6LyRqJBjC)l(0oCRyToOq$+JwCrBV=?~8{YWRd0qS5X8uYVa|xi%oz zyAE*OH`iQc1>t3DZyzwCi{xuP7ul9cOyorKJ95 z*Ol4IQz~@6cFal3L62jP;4~H-nGFDP4-qq#06fNS^zqyueZ#(e#+7ek$35j>e)w_5 z8_l$-asS2BljF982cFY*-4HC<4j~FhGJXTIUJB7MK+>wdaR)YgsD#UfDY(%#_5xqVw%j z`N~y*dgd7ctAOF9$>)PrNdW9*AdnCQMEin>LWX5FkshF)_%&otkNVfF&0HK8m) z6t?*I=i7xFCg~)<13PM%#$6(k#?JMP)(NW`dY{_PZ-MA6s3Jn6Tt%PMz=z}wPM^+= z%aa##5O=?-eIFDuO0>kvQO=%#S`k%;(I_YQv>;Bd)wu;AcBYSc7eY0Z2^p5~P66Id zuYTT=kMl6c@I?RRr&EZ5=~sVb+aTv*ZEQ*CmI43}Ig&yMGg$t1Jae}QKau}~M@i{q zr`ogn9p2~ZPbKd?e@o7RCvwsOC#^OZ`YR7`rsnD@B^J4n!i%-bi_lj~$D+S-wt^S~ zI6xs60MTQJaVzft1%XddZa*(*9zzf+_ky0YT{Dzyj6Vx~s|pDZ2>~7(MlFEYBSRMm zrVG(AI2cI^6%lM+;_uhhFA<*uj=om)=bAlBB6DMPlz@7Q4D^NGmauoB2pqX(jk;kLs5Dsv=FHwkc1W>#Jk^Z zbLX46|Ln~Ev9mk-zPr!!{F+C@4Q7B(-Jt!2C-QY_)@7s+WY549RObWfIBF1a0}J3r zaq0rtt97&UlgrJHGmeuiRX@lUGBP|>R~{k&HP$-;Sm)zPn73x^gBU8u42@Bih!l#O zXY0A`Pve~lV7i-a&!op$H$D*U!Q;r7f7>?LtBkIjgHFR2xKFnzGGa#r1PMkd+fZ{r;7{FP~^`KWB1(%iBO7HQS-v9 z2YD*Q$|+RLfo5#>mCT0ro72xvKDx^)1dXfzeUCfiAEDdAIgCIFduK%(YRp6t_3h_{ zkH{3>0${+QIufPUvmCJbwew5~xfcg5rToH~p(47*e{o#+*X&~yK!jUSPJl%hIS0@% zw~;73Q4hD*2MX<3;;o$$ZdF2-?lT853YgWV=3u|PgJpy_0~?7zDJLb5&{)XlrHTnc zg7|7!m;9>19X6JQC*Pl<=DvQi`R8ra*^i*K$2hfS{6nD(xu@h7J>g8i2%~Hg?v73< zT7t~CH6*~%HjvUxl}Xm$hh>S*s&}i9j3#SIbB9;p#Wqkt)(49`Xq+#|zM|yO07t)EV4A z8}u$mZM#7&dyx&P1u&QGJ?{Xgkc#Bn;bSFh-CPVJ7gbePa*nHR8Ytm9mgLeOwWrfW zPm6BvtXZ%`+kQ~x=a8+-qddv~a>G$f)f9d&Jj-SCfXbjNYm)W?rr?ubhl*S}nqFRY zP84coZpa=-P!H&jeb0HJVv}q(BqU{FHKnEx7&w(s0MeB-5=w8H&)iyf)LOH%%kBnAJCHYP+z zeRlqN6~t=d`8y3nGT?hd>~H2vjk)y7a!M9xQi3!@Ts7;QV+htn%+WK7CDxuF*bJ2$ zvGwQN`nh7$s=rcYRQ>faxIig;Z z(Je^6*E8_f+JZvjFPx7$p-+Ggp$qKJe}Nn6B2X(ud7{LTq*iMp_akC*H8z(+N^lAP zsQvRagePYA+pF}*y7Xv%vR|Ie1vqho#u!&ZeNabqiL3lW=LCxhTGAj2RLE>bfSMrv zyG5?gWS;>EG~e_GIxgXEF|W7A0MF78L*QLqo^LS1fXyjUa#U>?dr2Mgu+>qXb6ni@UcEGLu$Sl`x0_{{I+gnSgJa$; zp2C{7MrJ`e{6l^iL6-D~&i4F!gaSUFpK834!a{+Wkys{PxLas&)?_Y??ee6N-)CA| z$yZB8c({D!>>OpNprK*)GYOiG_KDLd!3ZI$2C=B)el;N){8yBzwOLi7kzg9 zIEv>|8l70;N&>23gMcE59UW2-V(5{CrAx$!tW`bq>!|=CCO5hHhT_|Odt&={(*5YZ zGQV_Ef6kCRKP*+{)a)w!ug>3%8FxkP8B|n+qn5KzLyuB{CHoNuIWKLegTjtHDH%jC z_Pw0|F**N8`mcdvO31cEE|(--#4y3XgM&ddqxSGCo8Wo2rn`Oeat&M084LjPYBzeA zP;B^2Qk@j2{ia&1tphfLPeQ~)MZ6>GaJ?;lpT;ZG`JUSglU6;J1aUdCXKGAKYWK!H$-x+z@e9Px+lcAP@^ zmFa=KVPE|rX7`cwfenVX*AYMM8?p$XGcHO44vnhS>|HeKI?fsmz8P;_Q>UB?^R>#k z@iAOS06BUkpCEJ=Mw`g5x63+@B!!|aOx>0t+$L_P7BLVf=0>_}>rK&$_h-e@jBIw= z0j$kN(<3EHs5ZB6Cx1nV9U6$~2rBC~8GTKxuZWB2dD*)5?`JR_OV0*f2qWD|@uylY z;nJZ{v7OG9i{I>H=W`_LbX|PXONKRC!I6oIVM6$g){}q<2~5| z2Ivm8GjneNX!PpSXgt{JmDO(;vLJ#QlNY=al1iKMr*V;t<~AG@Ta1ER`eO%ZRpZw> z`e`JoJecjw9Jb8&ComPtpAi~=BKl8Gf3yax!Sr!b8^rabLLnA##k;M?Ct1^}&)QZO zv{!R;d>`U&eGFC+0H~QsO72Qe)KiYX9OkE$$F&{FRWcf;sf#tB8dmv+LbjU$1>!OU zuZD@B$+Q`fQ z37GuXVZ4o$m2fC^ai%LfCB9xF>3&bTbIDM+Fc_2eOiftK{Z$&2J`JMuzeK6Zw3`&x zvsJP9f>Eg;5%spIuBv~GBh!L~5s!8uoA#W#*{m^;6%=FdZ}sUjsR=Lj1ZN?In9;nY zKXkgM9+?U7_od*9wyE~FD8I&!?%*6`e@w+8I5#Jg_*+=NKUypfq@B|&?Ngp) zvqH=A#-@9V;}R_+C8aeI6pD>i!_$C>^Hs zxfHCR%>LBGb-}kn%}nuxT6lK=K6>gunK5c?BJ=<8bbT?LK-7m){D+jLN}BQ>e(dEL zcnvBm7A%1~ zy_Zs;Ljg#wr%q0F`g2{iD+l~o9scb7p};?My$>SbKCE!bwTyP?{h-8998X_^tgY3S znS~E3f73fU(Jt{+srw1ZBBj*Modc@(Jl}--I$tP3&m(nfba$nP?sAdW!`tD*I-Ex6 z>$a399a%SL;;i%v=FFD%HhJTy4 z&$mbUE&arY>Z<2juARHe{YUZmUmxrLK4rjQ7G4ZzHvFhdd)n!g#4Q@PNrxx|H0HN! zDUc%Ncy%2~pY3sQRvs3!Hi7LE2fc{F&002p#D+IW%>Q(S>~9@=lhtHvrl14CT&?DMiDt1TQ;uKNU10+p>bF%_ zq|a_$Ae>rGYn%MufI;A|8mP*L>d)@(`ySz!oBQ04UF=gli-+edDZxe^E+|;BxMpks z=BfX=jg@u5!cQL`o_+mrpB9wnNO5rMl$hAyLFR^K3s3oOiZ|s;IkbI{vvFK_QhoE1 z@r$KrckL>!-jOQOB1ES$SBLph?yFXeK1SSmYE@vt|6|^_ z$LC$J`b?>vRbsf7_Fm=|ro4Wa+NOpn=+l{Vz>dsN3FqPKS#b*Z1LT)38vPh9nlJc< zt^3Dk!o{thx*5_VTSQ*@4MPi!C>fkf&G%yvHsB>Wv=b5;<+Io)*~jCscOY`pw^9ljPv19YXySMg|=Jil0IEzo1x}{`Qms%!$-UeMSiL2DRzDvFwTf; zvy?5c#rRE?0B761717xpKB3;t#j}|!4)0HUc}+qO4!3AJhFqwc?B)UxY-=c4s6^6RvJEz<$`5hR|%U$YEsw#Z9!57@DgG5VwJ{YHDKN)pOg9_kV zzi3Y$5)9RzR$3QC8xZGyEM^iUXNT>_bRw<<3m-7EGHf?ljf30lztHLq^iLACg)tU> zp;9OW-n4c~UG_WF@^Q@bmT{?Fku=Umu(hLx_orpKXw#fV5+`Al)az@2m3qw78)LQT zI#kCd|KYw4FSDMI!ZNq6s> zbAEk^KXRhrm_aOY-{K&F3F|+$*j0whxgQusm=#>`u~UCThJy`Irp=RyU&F&mL7flm z;=ZY8HUMq^Bqw0`UNdBrr(>kqTK)nef$EY>Urb6JXkOi7f^VIMxq2aP1-c2F{z`fK z@Rpau=)tzC;^u0DS!v~qdcq=&fmP6Ud_8^4pPIGm2u<(%NXe1z&X18*P*U3TTzo5D zjqq!7@)P>i@Jt0QFP74otsH~t4pC4JnvYE6_|ZRBk;7jNTOA;bruXU3u;@-g3?^%9 zY^!x%Ihf>_h3w8XVhvpS0&VXy()?>#oJ!W7ftm6k=1?!3{m~IQU{x&DE1-A{^|hMrJsnLN=G9 z0RM+Si~$qgppf>g__;_+I`gmDKXjRT9Rjq7j?_FWp^|>NWlv+v>@iOoY;8T5H&cGD zZ?12HX1^YyK(9aH5@VE#vj~%Fm7$Tc-gG)7Z=CgzANX`&Ca8MOD^hkKd94Y{=Tkh@ z$&Umr?y>ppB^9cw@bU^k_U;bn3PsGg6=i+F>}vS>{Cm)Tu8-eip&-mjFH+(alWgXy zE+tzmS;u7Xxm>soT!!zvQ`w&21M^eL=-MG{=TF!N{ zxR47V3b1C+-=FUn2~-v*GxcP9SVRbC&l_l2%^P0w{cs^8OVueolUF1)Z;{CI+HO09 z#8mEfYPMwb$#~gSg#GGHxT+$~eEoe5<-*IV5;X3li-Ez3}0lekVBe#0R4wL+rhUt&ixTEH!HThH{o&}WUVzXJN<>V^uv zSaQ>5{f49Jy*VvRhUk6>;6MPmyPLF4|DnsGs%{D~)-B(FzIjr<-8k|W)Mov3T8DXn zl`XR`@M$2^aX8yU@tFh>;yfPd9j7k!&A>^ps2LgaJozcJ7mxE)-)Vd3Z@7A7s$HcD z`K8m%>;Z+D%z=_q@0sd8v7uNmH4o~U+qsv)kp#g#G|~kg?ff#`M(zqjvfPEi^vGz; zy9a@N601g6ZS-z5m~N@zZofZ$UBI(|+TFzymhslBZf8$lRMPeoPC2#1YHDCJetqh0 zXR1&9heC<8{dK;?>~M@jCrsM6%0-gO6@quruk#CPH%O|J~^qkD{<`p}pKcOEzzs&6p|^d)hSMUxiF0X4?59 z1{=A#uh~-CVGkVVUOX6CN3)R{eNNc??9i4OlYM3;X~!^4kvBNm)Nb@oSpQL_e77AZ z(OswLL}|&lifeer{Rua}H>@S!dvrP@6Yhf8mU9g}?UEe7Kb`8d&^)5W??M;UaU=#` zHcb*&D&)#C!Vg5a{2CaBx1Ss48#auGalUNy*^+Y)vgBssY%y>rzdCjzXNX+r6U#e~ z$+6A9ww?Bet_xNM1|z%6>j?s;ALJ;GU{=!?Gbwa%tt`V3Na zWEm2i{=`~!V6Pa3kgso@2op!Hr6jiUeSfMDibvEeyVg28kM9!wSow0_`)W#ubWn=! z*Hs3R_n!P`>gT~dlOpu*D|5(v(P36Xc=-AdjIQzYv4|NOh4!_V9Ua;rK9(+-XUT0L zmg-M-y>y6~*{bF&VHx^W^2q;2475O8Jl)HtDDu!kwAgOZ)t0=+`d>q0d#e|2P9x0fB$~ zM!lx+Sm^rJylqHhY{6uJ>P?;Ghn`puPa{LXi}J0W7>Hs^xKiL(@}yJ(EvT2UensJ< zznAun3nQS5`8I0>(WNHh#+K6r5UMDIvrPN`hygP!DZ9O}a@opl=~8RCr{_t!fBdsQ36H|MJY;XdMiYidF`*BMIATko*j;r+Ytef@EmFPWvh0ORO zwl_TcSt{^%6*dj9)_Zg}Q6Yg6Frs_1pv6@>~@gvRq?5nq#nl(PF2=8m^Vq3M&)B#5Z#my9u*LZ(`tySf$+5&SF06K2j zOD7IESL#c*wglL3oSf#Ng=A8?!8*%@DnHBBeqK##J1qLxj|x$B67MD|3P4#@TI44U zPrv>rEr8CfA=9#PKRgdTZe0Wtj+XlcyI>$D_KLfq0^h1kWLve5odcg;-!Ud3_15Pi z@r{}15)s*<35aMzX+9b3 zef_+o(#UcdPQ5+_xH6$k9Y;dW+2rXPDVIGJa*Op~_0o$%<_msH6?4!=)9KH3BUju$ z&SCa0K4ZFM8`9GFt8bFEutZ{~aEDlGQV1;OoV4`RdEbZo?u|J|RQto(=8`at2}LG~ z*V3w|&5(IXJ2H9YA>7+6QLyi<=Xd0I^`SHe)KZ7^%4Cbi6Azuo{d}j!7nR_s=H0o^ zlM_A>U-s`N_9a|&D6qaAZ;)m1f~E&f=QS%AyFq|21m0YWZY!tC*OOv^_s(qxt_W^+ z#We}Dq>@j=3WeQpM8)CFpsbmBwJ_7MR;jCCJ-!5K9=E{q_xXvwG3s&|&MVkPX}aH@ zvF4ms^?O>qhRWndIlHpszq6zz+LMo9p!|WHl$gVCO|7>2sRKTOR(%WP8m#n*kN*d$ ze@yN^G9dIjSJ-EkpRTcfXEHDtW*EaH05U;(j z=*e4eH8Kk$1D(?@FEoj{GRpNtp5o%kvK;H_WqFk?unz$zVB&+)`}Tg{Y)(;l(gT86 z-w$Z~cLi&XN~A_R-TCC1nfH{AZziwRYdS(yqrz`ya*je_CnK z@SGKh9O2)8p=kp*5jU~$4p%rN(<*0sjsJCaZZ!P;p#4+Vv|jU$NN51BrK?ugH4IPp zEs$USV2%G-jh>FPVlRU4UH9!YWx;`4mvHS@t3vYDw2&x!ymnH-p3W&P749E^!Fbo= zLzMDMyXOL!Y-NOY2aC1*8c~exH{3&`;ly4C&Hd>vY_#U30Q>P;L`-4a#5DuGA%LjW zq0E*Al$p?ORIW`>6O*1C1yR%j-?}YhG7(h$iFbvY>J8@x#@=&~PLrH@N}=(o!plE& z1X()|GXLdgb~!~Tq0#MR=h*n;G}z2{v7KUUUM0fafVLe8Xg0PF5j&MDn?;s+t0bII z=gI&fxcpJ!AFV+XQ?DPTstoy=tA z^r~L$^n?)xDNuRbK#HmhNz5_-c{#Rw;%L7n{@L+?{-+tZiQTYTo93_eU3Dh}v;|z- zK)O?)Se4=P9hB4}ayL&m+94XpE8@g|mqFzaPO|JotPBu5#&?fahgZ3q#$bMHpSp6I zZuvVquHaHT_v~JHyTuhe828$$lp32~PD(BdtJ~JjwuvZ*P2ypI^pVTGoqZBVWSPqO z)czs+%&^rVRi&$?=lQUMZ-%$!r%(48V1=T_?D6tOs}KV<6Lw@m zXn8__b^2d2%53ddK~{$&0^5m#^hz|VB<^5@=NaYrw6fvb8hg%dOy|jax41 zsWbbCTaDE*J;%tH*wsmn)zb}=yzSNHKrU!2x_ia9`Q@8XW