From 89c3175d717917a0f77c243872df000d93f11142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 30 Dec 2020 12:37:28 +0100 Subject: [PATCH 001/104] Add fallback if getnameinfo fails --- lib/include/chiaki/session.h | 2 +- lib/src/session.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index db7bf02..1706e27 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -155,7 +155,7 @@ typedef struct chiaki_session_t bool ps5; struct addrinfo *host_addrinfos; struct addrinfo *host_addrinfo_selected; - char hostname[128]; + char hostname[256]; char regist_key[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t morning[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t did[CHIAKI_RP_DID_SIZE]; diff --git a/lib/src/session.c b/lib/src/session.c index de2f4e2..a57ea1d 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -596,11 +596,11 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch set_port(sa, htons(SESSION_PORT)); // TODO: this can block, make cancelable somehow - int r = getnameinfo(sa, (socklen_t)ai->ai_addrlen, session->connect_info.hostname, sizeof(session->connect_info.hostname), NULL, 0, 0); + int r = getnameinfo(sa, (socklen_t)ai->ai_addrlen, session->connect_info.hostname, sizeof(session->connect_info.hostname), NULL, 0, NI_NUMERICHOST); if(r != 0) { - free(sa); - continue; + CHIAKI_LOGE(session->log, "getnameinfo failed with %s, filling the hostname with fallback", gai_strerror(r)); + memcpy(session->connect_info.hostname, "unknown", 8); } CHIAKI_LOGI(session->log, "Trying to request session from %s:%d", session->connect_info.hostname, SESSION_PORT); From 85d9594ebce06c7c888486760fdb3cec95277dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 30 Dec 2020 16:11:06 +0100 Subject: [PATCH 002/104] Add DualSense to Setsu --- setsu/src/setsu.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index f899d07..819b174 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -157,8 +157,9 @@ static bool is_device_interesting(struct udev_device *dev) { static const uint32_t device_ids[] = { // vendor id, model id - 0x054c, 0x05c4, // DualShock 4 Gen 1 USB - 0x054c, 0x09cc // DualShock 4 Gen 2 USB + 0x054c, 0x05c4, // DualShock 4 Gen 1 + 0x054c, 0x09cc, // DualShock 4 Gen 2 + 0x54c, 0x0ce6 // DualSense }; // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: From fbb19f94ead9472f21890a730484f0ea73589a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 31 Dec 2020 21:24:30 +0100 Subject: [PATCH 003/104] Fix err in streamsession.cpp for pi --- gui/src/streamsession.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index fedae10..f5c4c89 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -52,6 +52,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje audio_io(nullptr) { connected = false; + ChiakiErrorCode err; #if CHIAKI_LIB_ENABLE_PI_DECODER if(connect_info.decoder == Decoder::Pi) @@ -66,7 +67,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje ffmpeg_decoder = new ChiakiFfmpegDecoder; ChiakiLogSniffer sniffer; chiaki_log_sniffer_init(&sniffer, CHIAKI_LOG_ALL, GetChiakiLog()); - ChiakiErrorCode err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, + err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, chiaki_log_sniffer_get_log(&sniffer), chiaki_target_is_ps5(connect_info.target) ? connect_info.video_profile.codec : CHIAKI_CODEC_H264, connect_info.hw_decoder.isEmpty() ? NULL : connect_info.hw_decoder.toUtf8().constData(), From 81984b7d48b7459155353833b04016aa0f83a4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 31 Dec 2020 23:45:47 +0100 Subject: [PATCH 004/104] Add Rumble to Lib --- gui/src/streamsession.cpp | 7 +++++++ lib/include/chiaki/session.h | 8 ++++++++ lib/include/chiaki/takion.h | 1 + lib/src/streamconnection.c | 35 +++++++++++++++++++++++++++++++++-- lib/src/takion.c | 5 +++-- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index f5c4c89..886806f 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -381,6 +381,13 @@ void StreamSession::Event(ChiakiEvent *event) case CHIAKI_EVENT_LOGIN_PIN_REQUEST: emit LoginPINRequested(event->login_pin_request.pin_incorrect); break; + case CHIAKI_EVENT_RUMBLE: + // TODO + //CHIAKI_LOGD(GetChiakiLog(), "Rumble %#02x, %#02x, %#02x", + // event->rumble.unknown, event->rumble.left, event->rumble.right); + break; + default: + break; } } diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 1706e27..a3480c1 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -114,6 +114,12 @@ typedef struct chiaki_audio_stream_info_event_t ChiakiAudioHeader audio_header; } ChiakiAudioStreamInfoEvent; +typedef struct chiaki_rumble_event_t +{ + uint8_t unknown; + uint8_t left; + uint8_t right; +} ChiakiRumbleEvent; typedef enum { CHIAKI_EVENT_CONNECTED, @@ -121,6 +127,7 @@ typedef enum { CHIAKI_EVENT_KEYBOARD_OPEN, CHIAKI_EVENT_KEYBOARD_TEXT_CHANGE, CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE, + CHIAKI_EVENT_RUMBLE, CHIAKI_EVENT_QUIT, } ChiakiEventType; @@ -131,6 +138,7 @@ typedef struct chiaki_event_t { ChiakiQuitEvent quit; ChiakiKeyboardEvent keyboard; + ChiakiRumbleEvent rumble; struct { bool pin_incorrect; // false on first request, true if the pin entered before was incorrect diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index 3f7e96f..1715842 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -26,6 +26,7 @@ extern "C" { typedef enum chiaki_takion_message_data_type_t { CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF = 0, + CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE = 7, CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9 } ChiakiTakionMessageDataType; diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index c8ed43c..28b22dc 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -45,6 +45,8 @@ void chiaki_session_send_event(ChiakiSession *session, ChiakiEvent *event); static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user); static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream_connection); static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection *stream_connection); static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); @@ -368,9 +370,21 @@ static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user) static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size) { - if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF) - return; + switch(data_type) + { + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF: + stream_connection_takion_data_protobuf(stream_connection, buf, buf_size); + break; + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE: + stream_connection_takion_data_rumble(stream_connection, buf, buf_size); + break; + default: + break; + } +} +static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ chiaki_mutex_lock(&stream_connection->state_mutex); switch(stream_connection->state) { @@ -385,6 +399,23 @@ static void stream_connection_takion_data(ChiakiStreamConnection *stream_connect break; } chiaki_mutex_unlock(&stream_connection->state_mutex); + +} + +static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ + if(buf_size < 3) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection got rumble packet with size %#llx < 3", + (unsigned long long)buf_size); + return; + } + ChiakiEvent event = { 0 }; + event.type = CHIAKI_EVENT_RUMBLE; + event.rumble.unknown = buf[0]; + event.rumble.left = buf[1]; + event.rumble.right = buf[2]; + chiaki_session_send_event(stream_connection->session, &event); } static void stream_connection_takion_data_handle_disconnect(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) diff --git a/lib/src/takion.c b/lib/src/takion.c index 48a1537..fe3e95b 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -56,7 +56,6 @@ typedef enum takion_packet_type_t { TAKION_PACKET_TYPE_HANDSHAKE = 4, TAKION_PACKET_TYPE_CONGESTION = 5, TAKION_PACKET_TYPE_FEEDBACK_STATE = 6, - TAKION_PACKET_TYPE_RUMBLE_EVENT = 7, TAKION_PACKET_TYPE_CLIENT_INFO = 8, TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9 } TakionPacketType; @@ -961,7 +960,9 @@ static void takion_flush_data_queue(ChiakiTakion *takion) if(zero_a != 0) CHIAKI_LOGW(takion->log, "Takion received data with unexpected nonzero %#x at buf+6", zero_a); - if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) + if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) { CHIAKI_LOGW(takion->log, "Takion received data with unexpected data type %#x", data_type); chiaki_log_hexdump(takion->log, CHIAKI_LOG_WARNING, entry->packet_buf, entry->packet_size); From 6c46920adbe2b8906cb2e4285b38d5c997332ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 11:09:13 +0100 Subject: [PATCH 005/104] Fix some Warnings --- test/reorderqueue.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/reorderqueue.c b/test/reorderqueue.c index e5a90c9..f0fd5fe 100644 --- a/test/reorderqueue.c +++ b/test/reorderqueue.c @@ -16,7 +16,7 @@ typedef struct drop_record_t static void drop(uint64_t seq_num, void *elem_user, void *cb_user) { DropRecord *record = cb_user; - uint64_t v = (uint64_t)elem_user; + uint64_t v = (uint64_t)(size_t)elem_user; if(v > DROP_RECORD_MAX) { record->failed = true; @@ -58,7 +58,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); munit_assert(!drop_record.failed); munit_assert_uint64(drop_record.count[0], ==, 0); - munit_assert_uint64((uint64_t)user, ==, 0); + munit_assert_uint64((uint64_t)(size_t)user, ==, 0); munit_assert_uint64(seq_num, ==, 42); // push outdated @@ -105,22 +105,22 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 44); - munit_assert_uint64((uint64_t)user, ==, 3); + munit_assert_uint64((uint64_t)(size_t)user, ==, 3); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 45); - munit_assert_uint64((uint64_t)user, ==, 2); + munit_assert_uint64((uint64_t)(size_t)user, ==, 2); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 46); - munit_assert_uint64((uint64_t)user, ==, 1); + munit_assert_uint64((uint64_t)(size_t)user, ==, 1); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 47); - munit_assert_uint64((uint64_t)user, ==, 5); + munit_assert_uint64((uint64_t)(size_t)user, ==, 5); // should be empty now again pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); @@ -142,7 +142,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 1337); - munit_assert_uint64((uint64_t)user, ==, 6); + munit_assert_uint64((uint64_t)(size_t)user, ==, 6); munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); // same as before, but with an element in the queue that will be dropped @@ -163,7 +163,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 2000); - munit_assert_uint64((uint64_t)user, ==, 8); + munit_assert_uint64((uint64_t)(size_t)user, ==, 8); munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); chiaki_reorder_queue_fini(&queue); @@ -182,4 +182,4 @@ MunitTest tests_reorder_queue[] = { NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } -}; \ No newline at end of file +}; From 3c2e9a0418f5fec6ea4bd656791db6e2a16522df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 11:52:49 +0100 Subject: [PATCH 006/104] Use correct Target in GUI CLI Start --- gui/src/main.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gui/src/main.cpp b/gui/src/main.cpp index 56a85f2..b6731c8 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -123,6 +123,7 @@ int real_main(int argc, char *argv[]) QString host = args[args.size()-1]; QByteArray morning; QByteArray regist_key; + ChiakiTarget target = CHIAKI_TARGET_PS4_10; if(parser.value(regist_key_option).isEmpty() && parser.value(morning_option).isEmpty()) { @@ -134,6 +135,7 @@ int real_main(int argc, char *argv[]) { morning = temphost.GetRPKey(); regist_key = temphost.GetRPRegistKey(); + target = temphost.GetTarget(); break; } printf("No configuration found for '%s'\n", args[1].toLocal8Bit().constData()); @@ -142,6 +144,7 @@ int real_main(int argc, char *argv[]) } else { + // TODO: explicit option for target regist_key = parser.value(regist_key_option).toUtf8(); if(regist_key.length() > sizeof(ChiakiConnectInfo::regist_key)) { @@ -161,8 +164,7 @@ int real_main(int argc, char *argv[]) return 1; } } - // TODO: target here - StreamSessionConnectInfo connect_info(&settings, CHIAKI_TARGET_PS4_10, host, regist_key, morning, parser.isSet(fullscreen_option)); + StreamSessionConnectInfo connect_info(&settings, target, host, regist_key, morning, parser.isSet(fullscreen_option)); return RunStream(app, connect_info); } #ifdef CHIAKI_ENABLE_CLI From 042e02eb3eac4ebc6594fd586b0850734354f980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 13:56:30 +0100 Subject: [PATCH 007/104] Add Rumble to GUI --- gui/include/controllermanager.h | 1 + gui/include/streamsession.h | 2 +- gui/src/controllermanager.cpp | 9 +++++++++ gui/src/streamsession.cpp | 14 ++++++++------ lib/include/chiaki/session.h | 4 ++-- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 6ec6efb..18d9d72 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -73,6 +73,7 @@ class Controller : public QObject int GetDeviceID(); QString GetName(); ChiakiControllerState GetState(); + void SetRumble(uint8_t left, uint8_t right); signals: void StateChanged(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index c98758e..47dc217 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -92,13 +92,13 @@ class StreamSession : public QObject QMap key_map; void PushAudioFrame(int16_t *buf, size_t samples_count); - void Event(ChiakiEvent *event); #if CHIAKI_GUI_ENABLE_SETSU void HandleSetsuEvent(SetsuEvent *event); #endif private slots: void InitAudio(unsigned int channels, unsigned int rate); + void Event(ChiakiEvent *event); public: explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr); diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 2f9a2aa..16804af 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -291,3 +291,12 @@ ChiakiControllerState Controller::GetState() #endif return state; } + +void Controller::SetRumble(uint8_t left, uint8_t right) +{ +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + if(!controller) + return; + SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000); +#endif +} diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 886806f..b1cbb47 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -313,13 +313,11 @@ void StreamSession::SendFeedbackState() ChiakiControllerState state; chiaki_controller_state_set_idle(&state); -#if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER for(auto controller : controllers) { auto controller_state = controller->GetState(); chiaki_controller_state_or(&state, &state, &controller_state); } -#endif #if CHIAKI_GUI_ENABLE_SETSU chiaki_controller_state_or(&state, &state, &setsu_state); @@ -381,11 +379,15 @@ void StreamSession::Event(ChiakiEvent *event) case CHIAKI_EVENT_LOGIN_PIN_REQUEST: emit LoginPINRequested(event->login_pin_request.pin_incorrect); break; - case CHIAKI_EVENT_RUMBLE: - // TODO - //CHIAKI_LOGD(GetChiakiLog(), "Rumble %#02x, %#02x, %#02x", - // event->rumble.unknown, event->rumble.left, event->rumble.right); + case CHIAKI_EVENT_RUMBLE: { + uint8_t left = event->rumble.left; + uint8_t right = event->rumble.right; + QMetaObject::invokeMethod(this, [this, left, right]() { + for(auto controller : controllers) + controller->SetRumble(left, right); + }); break; + } default: break; } diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index a3480c1..3a60417 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -117,8 +117,8 @@ typedef struct chiaki_audio_stream_info_event_t typedef struct chiaki_rumble_event_t { uint8_t unknown; - uint8_t left; - uint8_t right; + uint8_t left; // low-frequency + uint8_t right; // high-frequency } ChiakiRumbleEvent; typedef enum { From 49d65ad14a418c0409fbb4f87484ddc3b8e17b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 15:24:53 +0100 Subject: [PATCH 008/104] Minor Style Fixes --- gui/src/avopenglwidget.cpp | 3 +- gui/src/controllermanager.cpp | 1 - gui/src/main.cpp | 2 +- gui/src/mainwindow.cpp | 4 +- gui/src/settings.cpp | 12 +- gui/src/settingsdialog.cpp | 12 +- gui/src/settingskeycapturedialog.cpp | 4 +- lib/src/audioreceiver.c | 8 +- lib/src/base64.c | 57 ++++--- lib/src/common.c | 6 +- lib/src/congestioncontrol.c | 2 +- lib/src/controller.c | 14 +- lib/src/ctrl.c | 7 +- lib/src/ecdh.c | 16 +- lib/src/frameprocessor.c | 4 +- lib/src/gkcrypt.c | 43 ++--- lib/src/http.c | 1 - lib/src/packetstats.c | 1 - lib/src/random.c | 6 +- lib/src/regist.c | 1 - lib/src/rpcrypt.c | 4 +- lib/src/senkusha.c | 2 - lib/src/session.c | 7 +- lib/src/stoppipe.c | 3 +- lib/src/streamconnection.c | 10 +- lib/src/takion.c | 26 +-- lib/src/thread.c | 20 +-- switch/src/discoverymanager.cpp | 18 +-- switch/src/gui.cpp | 16 +- switch/src/io.cpp | 4 +- switch/src/main.cpp | 6 +- switch/src/settings.cpp | 232 +++++++++++++-------------- 32 files changed, 248 insertions(+), 304 deletions(-) diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 3d1d8eb..857af09 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -109,7 +109,6 @@ static const float vert_pos[] = { 1.0f, 1.0f }; - QSurfaceFormat AVOpenGLWidget::CreateSurfaceFormat() { QSurfaceFormat format; @@ -129,7 +128,7 @@ AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent) { enum AVPixelFormat pixel_format = chiaki_ffmpeg_decoder_get_pixel_format(session->GetFfmpegDecoder()); conversion_config = nullptr; - for(auto &cc: conversion_configs) + for(auto &cc : conversion_configs) { if(pixel_format == cc.pixel_format) { diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 16804af..a78fd44 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -68,7 +68,6 @@ static QSet chiaki_motion_controller_guids({ "030000008f0e00001431000000000000", }); - static ControllerManager *instance = nullptr; #define UPDATE_INTERVAL_MS 4 diff --git a/gui/src/main.cpp b/gui/src/main.cpp index b6731c8..010e744 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -111,7 +111,7 @@ int real_main(int argc, char *argv[]) if(args[0] == "list") { for(const auto &host : settings.GetRegisteredHosts()) - printf("Host: %s \n", host.GetServerNickname().toLocal8Bit().constData()); + printf("Host: %s \n", host.GetServerNickname().toLocal8Bit().constData()); return 0; } if(args[0] == "stream") diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index 296626d..aa76ab3 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -167,7 +167,7 @@ MainWindow::MainWindow(Settings *settings, QWidget *parent) grid_widget->setContentsMargins(0, 0, 0, 0); resize(800, 600); - + connect(&discovery_manager, &DiscoveryManager::HostsUpdated, this, &MainWindow::UpdateDisplayServers); connect(settings, &Settings::RegisteredHostsUpdated, this, &MainWindow::UpdateDisplayServers); connect(settings, &Settings::ManualHostsUpdated, this, &MainWindow::UpdateDisplayServers); @@ -330,7 +330,7 @@ void MainWindow::UpdateDisplayServers() display_servers.append(server); } - + UpdateServerWidgets(); } diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 17f7d83..6d296d1 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -88,10 +88,10 @@ uint32_t Settings::GetLogLevelMask() } static const QMap resolutions = { - { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p"} + { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p" } }; static const ChiakiVideoResolutionPreset resolution_default = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; @@ -136,8 +136,8 @@ void Settings::SetBitrate(unsigned int bitrate) } static const QMap codecs = { - { CHIAKI_CODEC_H264, "h264"}, - { CHIAKI_CODEC_H265, "h265"} + { CHIAKI_CODEC_H264, "h264" }, + { CHIAKI_CODEC_H265, "h265" } }; static const ChiakiCodec codec_default = CHIAKI_CODEC_H265; diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index b8bd412..3f8856f 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -89,7 +89,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa connect(disconnect_action_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(DisconnectActionSelected())); general_layout->addRow(tr("Action on Disconnect:"), disconnect_action_combo_box); - + audio_device_combo_box = new QComboBox(this); audio_device_combo_box->addItem(tr("Auto")); auto current_audio_device = settings->GetAudioOutDevice(); @@ -112,7 +112,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa auto available_devices = audio_devices_future_watcher->result(); while(audio_device_combo_box->count() > 1) // remove all but "Auto" audio_device_combo_box->removeItem(1); - for (QAudioDeviceInfo di : available_devices) + for(QAudioDeviceInfo di : available_devices) audio_device_combo_box->addItem(di.deviceName(), di.deviceName()); int audio_out_device_index = audio_device_combo_box->findData(settings->GetAudioOutDevice()); audio_device_combo_box->setCurrentIndex(audio_out_device_index < 0 ? 0 : audio_out_device_index); @@ -136,10 +136,10 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa resolution_combo_box = new QComboBox(this); static const QList> resolution_strings = { - { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS5 and PS4 Pro only)"} + { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS5 and PS4 Pro only)" } }; auto current_res = settings->GetResolution(); for(const auto &p : resolution_strings) diff --git a/gui/src/settingskeycapturedialog.cpp b/gui/src/settingskeycapturedialog.cpp index 3339813..a631444 100644 --- a/gui/src/settingskeycapturedialog.cpp +++ b/gui/src/settingskeycapturedialog.cpp @@ -7,7 +7,7 @@ #include #include -SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget* parent) +SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget *parent) { setWindowTitle(tr("Key Capture")); @@ -23,7 +23,7 @@ SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget* parent) connect(button, &QPushButton::clicked, this, &QDialog::accept); } -void SettingsKeyCaptureDialog::keyReleaseEvent(QKeyEvent* event) +void SettingsKeyCaptureDialog::keyReleaseEvent(QKeyEvent *event) { KeyCaptured(Qt::Key(event->key())); accept(); diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 5808e4c..9fec69d 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -23,7 +23,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *au return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT void chiaki_audio_receiver_fini(ChiakiAudioReceiver *audio_receiver) { #ifdef CHIAKI_LIB_ENABLE_OPUS @@ -32,7 +31,6 @@ CHIAKI_EXPORT void chiaki_audio_receiver_fini(ChiakiAudioReceiver *audio_receive chiaki_mutex_fini(&audio_receiver->mutex); } - CHIAKI_EXPORT void chiaki_audio_receiver_stream_info(ChiakiAudioReceiver *audio_receiver, ChiakiAudioHeader *audio_header) { chiaki_mutex_lock(&audio_receiver->mutex); @@ -79,15 +77,15 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re if(packet->data_size != (size_t)unit_size * (size_t)packet->units_in_frame_total) { CHIAKI_LOGE(audio_receiver->log, "Audio AV Packet size mismatch %#llx vs %#llx", - (unsigned long long)packet->data_size, - (unsigned long long)(unit_size * packet->units_in_frame_total)); + (unsigned long long)packet->data_size, + (unsigned long long)(unit_size * packet->units_in_frame_total)); return; } if(packet->frame_index > (1 << 15)) audio_receiver->frame_index_startup = false; - for(size_t i=0; i> 18) & 63; @@ -46,7 +45,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // if we have only two bytes available, then their encoding is // spread out over three chars - if((x+1) < in_size) + if((x + 1) < in_size) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -55,7 +54,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // if we have all three bytes available, then their encoding is spread // out over four characters - if((x+2) < in_size) + if((x + 2) < in_size) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -65,9 +64,9 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // create and add padding that is required if we did not have a multiple of 3 // number of characters available - if (pad_count > 0) + if(pad_count > 0) { - for (; pad_count < 3; pad_count++) + for(; pad_count < 3; pad_count++) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -80,25 +79,22 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ return CHIAKI_ERR_SUCCESS; } - - - #define WHITESPACE 64 -#define EQUALS 65 -#define INVALID 66 +#define EQUALS 65 +#define INVALID 66 static const unsigned char d[] = { - 66,66,66,66,66,66,66,66,66,66,64,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,62,66,66,66,63,52,53, - 54,55,56,57,58,59,60,61,66,66,66,65,66,66,66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,66,66,66,66,66,66,26,27,28, - 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66 + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 64, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 62, 66, 66, 66, 63, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 66, 66, 66, 65, 66, 66, 66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 66, 66, 66, 66, 66, 66, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66 }; CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_size, uint8_t *out, size_t *out_size) @@ -108,17 +104,17 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz uint32_t buf = 0; size_t len = 0; - while (in < end) + while(in < end) { unsigned char c = d[(size_t)(*in++)]; switch(c) { case WHITESPACE: - continue; // skip whitespace + continue; // skip whitespace case INVALID: - return CHIAKI_ERR_INVALID_DATA; // invalid input - case EQUALS: // pad character, end of data + return CHIAKI_ERR_INVALID_DATA; // invalid input + case EQUALS: // pad character, end of data in = end; continue; default: @@ -132,7 +128,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz *(out++) = (unsigned char)((buf >> 16) & 0xff); *(out++) = (unsigned char)((buf >> 8) & 0xff); *(out++) = (unsigned char)(buf & 0xff); - buf = 0; iter = 0; + buf = 0; + iter = 0; } } } diff --git a/lib/src/common.c b/lib/src/common.c index 0dd2006..adb0fc6 100644 --- a/lib/src/common.c +++ b/lib/src/common.c @@ -1,14 +1,14 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include -#include #include +#include #include +#include #include #include -#include #ifdef _WIN32 #include @@ -98,7 +98,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_lib_init() WORD wsa_version = MAKEWORD(2, 2); WSADATA wsa_data; int err = WSAStartup(wsa_version, &wsa_data); - if (err != 0) + if(err != 0) return CHIAKI_ERR_NETWORK; } #endif diff --git a/lib/src/congestioncontrol.c b/lib/src/congestioncontrol.c index e478410..8fb912b 100644 --- a/lib/src/congestioncontrol.c +++ b/lib/src/congestioncontrol.c @@ -25,7 +25,7 @@ static void *congestion_control_thread_func(void *user) packet.received = (uint16_t)received; packet.lost = (uint16_t)lost; CHIAKI_LOGV(control->takion->log, "Sending Congestion Control Packet, received: %u, lost: %u", - (unsigned int)packet.received, (unsigned int)packet.lost); + (unsigned int)packet.received, (unsigned int)packet.lost); chiaki_takion_send_congestion(control->takion, &packet); } diff --git a/lib/src/controller.c b/lib/src/controller.c index b347bb7..0e3a522 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -14,7 +14,7 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state state->right_x = 0; state->right_y = 0; state->touch_id_next = 0; - for(size_t i=0; itouches[i].id = -1; state->touches[i].x = 0; @@ -24,7 +24,7 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y) { - for(size_t i=0; itouches[i].id < 0) { @@ -40,7 +40,7 @@ CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState * CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *state, uint8_t id) { - for(size_t i=0; itouches[i].id == id) { @@ -53,7 +53,7 @@ CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *sta CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y) { id &= TOUCH_ID_MASK; - for(size_t i=0; itouches[i].id == id) { @@ -64,8 +64,8 @@ CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState * } } -#define MAX(a, b) ((a) > (b) ? (a) : (b)) -#define ABS(a) ((a) > 0 ? (a) : -(a)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define ABS(a) ((a) > 0 ? (a) : -(a)) #define MAX_ABS(a, b) (ABS(a) > ABS(b) ? (a) : (b)) CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, ChiakiControllerState *a, ChiakiControllerState *b) @@ -79,7 +79,7 @@ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, Chiaki out->right_y = MAX_ABS(a->right_y, b->right_y); out->touch_id_next = 0; - for(size_t i=0; itouches[i].id >= 0 ? &a->touches[i] : (b->touches[i].id >= 0 ? &b->touches[i] : NULL); if(!touch) diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index f92d12b..9dfc277 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -501,7 +501,6 @@ static void ctrl_enable_optional_features(ChiakiCtrl *ctrl) ctrl_message_send(ctrl, 0x36, pre_enable, 4); } - static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) { if(ctrl->session->ctrl_session_id_received) @@ -652,7 +651,7 @@ static void ctrl_message_received_keyboard_close(ChiakiCtrl *ctrl, uint8_t *payl chiaki_session_send_event(ctrl->session, &keyboard_event); } -static void ctrl_message_received_keyboard_text_change(ChiakiCtrl* ctrl, uint8_t* payload, size_t payload_size) +static void ctrl_message_received_keyboard_text_change(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) { assert(payload_size >= sizeof(CtrlKeyboardTextResponseMessage)); @@ -676,7 +675,8 @@ static void ctrl_message_received_keyboard_text_change(ChiakiCtrl* ctrl, uint8_t free(buffer); } -typedef struct ctrl_response_t { +typedef struct ctrl_response_t +{ bool server_type_valid; uint8_t rp_server_type[0x10]; bool success; @@ -742,7 +742,6 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(session->log, "Failed to set ctrl socket to non-blocking: %s", chiaki_error_string(err)); - chiaki_mutex_unlock(&ctrl->notif_mutex); err = chiaki_stop_pipe_connect(&ctrl->notif_pipe, sock, sa, addr->ai_addrlen); chiaki_mutex_lock(&ctrl->notif_mutex); diff --git a/lib/src/ecdh.c b/lib/src/ecdh.c index 1a48b54..3277b60 100644 --- a/lib/src/ecdh.c +++ b/lib/src/ecdh.c @@ -79,7 +79,6 @@ CHIAKI_EXPORT void chiaki_ecdh_fini(ChiakiECDH *ecdh) #endif } - CHIAKI_EXPORT ChiakiErrorCode chiaki_ecdh_set_local_key(ChiakiECDH *ecdh, const uint8_t *private_key, size_t private_key_size, const uint8_t *public_key, size_t public_key_size) { #ifdef CHIAKI_LIB_ENABLE_MBEDTLS @@ -89,24 +88,19 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ecdh_set_local_key(ChiakiECDH *ecdh, const // public int r = 0; - r = mbedtls_ecp_point_read_binary(&ecdh->ctx.grp, &ecdh->ctx.Q, - public_key, public_key_size); - if(r != 0 ){ + r = mbedtls_ecp_point_read_binary(&ecdh->ctx.grp, &ecdh->ctx.Q, public_key, public_key_size); + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } // secret r = mbedtls_mpi_read_binary(&ecdh->ctx.d, private_key, private_key_size); - if(r != 0 ){ + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } // regen key - r = mbedtls_ecdh_gen_public(&ecdh->ctx.grp, &ecdh->ctx.d, - &ecdh->ctx.Q, mbedtls_ctr_drbg_random, &ecdh->drbg); - if(r != 0 ){ + r = mbedtls_ecdh_gen_public(&ecdh->ctx.grp, &ecdh->ctx.d, &ecdh->ctx.Q, mbedtls_ctr_drbg_random, &ecdh->drbg); + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } return CHIAKI_ERR_SUCCESS; #else diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index 058f6ac..c072a98 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -150,7 +150,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess CHIAKI_LOGE(frame_processor->log, "Packet's unit index is too high"); return CHIAKI_ERR_INVALID_DATA; } - + if(!packet->data_size) { CHIAKI_LOGW(frame_processor->log, "Unit is empty"); @@ -162,7 +162,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess CHIAKI_LOGW(frame_processor->log, "Unit is bigger than pre-calculated size!"); return CHIAKI_ERR_INVALID_DATA; } - + ChiakiFrameUnit *unit = frame_processor->unit_slots + packet->unit_index; if(unit->data_size) { diff --git a/lib/src/gkcrypt.c b/lib/src/gkcrypt.c index c99c5c2..e8dcb12 100644 --- a/lib/src/gkcrypt.c +++ b/lib/src/gkcrypt.c @@ -19,10 +19,8 @@ #include "utils.h" - #define KEY_BUF_CHUNK_SIZE 0x1000 - static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret); static void *gkcrypt_thread_func(void *user); @@ -110,7 +108,6 @@ CHIAKI_EXPORT void chiaki_gkcrypt_fini(ChiakiGKCrypt *gkcrypt) } } - static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret) { uint8_t data[3 + CHIAKI_HANDSHAKE_KEY_SIZE + 2]; @@ -123,37 +120,41 @@ static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, uint8_t hmac[CHIAKI_GKCRYPT_BLOCK_SIZE*2]; size_t hmac_size = sizeof(hmac); - #ifdef CHIAKI_LIB_ENABLE_MBEDTLS +#ifdef CHIAKI_LIB_ENABLE_MBEDTLS mbedtls_md_context_t ctx; mbedtls_md_init(&ctx); - if(mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256) , 1) != 0){ + if(mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_starts(&ctx, ecdh_secret, CHIAKI_ECDH_SECRET_SIZE) != 0){ + if(mbedtls_md_hmac_starts(&ctx, ecdh_secret, CHIAKI_ECDH_SECRET_SIZE) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_update(&ctx, data, sizeof(data)) != 0){ + if(mbedtls_md_hmac_update(&ctx, data, sizeof(data)) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_finish(&ctx, hmac) != 0){ + if(mbedtls_md_hmac_finish(&ctx, hmac) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } mbedtls_md_free(&ctx); - #else +#else if(!HMAC(EVP_sha256(), ecdh_secret, CHIAKI_ECDH_SECRET_SIZE, data, sizeof(data), hmac, (unsigned int *)&hmac_size)) return CHIAKI_ERR_UNKNOWN; - #endif +#endif assert(hmac_size == sizeof(hmac)); memcpy(gkcrypt->key_base, hmac, CHIAKI_GKCRYPT_BLOCK_SIZE); @@ -220,7 +221,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcry mbedtls_aes_context ctx; mbedtls_aes_init(&ctx); - if(mbedtls_aes_setkey_enc(&ctx, gkcrypt->key_base, 128) != 0){ + if(mbedtls_aes_setkey_enc(&ctx, gkcrypt->key_base, 128) != 0) + { mbedtls_aes_free(&ctx); return CHIAKI_ERR_UNKNOWN; } @@ -248,9 +250,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcry counter_add(cur, gkcrypt->iv, counter_offset++); #ifdef CHIAKI_LIB_ENABLE_MBEDTLS - for(int i=0; i #endif - CHIAKI_EXPORT void chiaki_http_header_free(ChiakiHttpHeader *header) { while(header) diff --git a/lib/src/packetstats.c b/lib/src/packetstats.c index c04adc8..c7da9d6 100644 --- a/lib/src/packetstats.c +++ b/lib/src/packetstats.c @@ -74,4 +74,3 @@ CHIAKI_EXPORT void chiaki_packet_stats_get(ChiakiPacketStats *stats, bool reset, reset_stats(stats); chiaki_mutex_unlock(&stats->mutex); } - diff --git a/lib/src/random.c b/lib/src/random.c index 974db60..158ce4d 100644 --- a/lib/src/random.c +++ b/lib/src/random.c @@ -28,10 +28,12 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_random_bytes_crypt(uint8_t *buf, size_t buf mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_set_prediction_resistance(&ctr_drbg, MBEDTLS_CTR_DRBG_PR_OFF); - if(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *) "RANDOM_GEN", 10 ) != 0 ){ + if(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)"RANDOM_GEN", 10) != 0) + { return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_ctr_drbg_random(&ctr_drbg, buf, buf_size) != 0){ + if(mbedtls_ctr_drbg_random(&ctr_drbg, buf, buf_size) != 0) + { return CHIAKI_ERR_UNKNOWN; } diff --git a/lib/src/regist.c b/lib/src/regist.c index 47c6ab1..af17942 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -160,7 +160,6 @@ static int request_header_format(char *buf, size_t buf_size, size_t payload_size return cur; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(ChiakiTarget target, const uint8_t *ambassador, uint8_t *buf, size_t *buf_size, ChiakiRPCrypt *crypt, const char *psn_online_id, const uint8_t *psn_account_id, uint32_t pin) { size_t buf_size_val = *buf_size; diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 197580e..30225c0 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -1488,7 +1488,7 @@ static ChiakiErrorCode bright_ambassador(ChiakiTarget target, uint8_t *bright, u 0x20, 0x98, 0xfd, 0x34, 0xca, 0x7a, 0x66, 0x20, 0x58, 0xd2, 0x36, 0x7f, 0x2b, 0xa7, 0xd1, 0xde, 0x6f, 0x36, 0xb4, 0xf2, 0x3b, 0x20, 0x5d, 0x02 - }; + }; if(target < CHIAKI_TARGET_PS4_10) return CHIAKI_ERR_INVALID_DATA; @@ -1865,7 +1865,6 @@ static const uint8_t *rpcrypt_hmac_key(ChiakiRPCrypt *rpcrypt) } } - #ifdef CHIAKI_LIB_ENABLE_MBEDTLS CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, uint8_t *iv, uint64_t counter) { @@ -1968,7 +1967,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, buf[CHIAKI_RPCRYPT_KEY_SIZE + 6] = (uint8_t)((counter >> 0x08) & 0xff); buf[CHIAKI_RPCRYPT_KEY_SIZE + 7] = (uint8_t)((counter >> 0x00) & 0xff); - uint8_t hmac[32]; unsigned int hmac_len = 0; if(!HMAC(EVP_sha256(), hmac_key, CHIAKI_RPCRYPT_KEY_SIZE, buf, sizeof(buf), hmac, &hmac_len)) diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index 1fa33d4..ebab5d7 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -645,7 +645,6 @@ static void senkusha_takion_data(ChiakiSenkusha *senkusha, ChiakiTakionMessageDa senkusha->state_finished = true; chiaki_cond_signal(&senkusha->state_cond); } - } chiaki_mutex_unlock(&senkusha->state_mutex); } @@ -878,4 +877,3 @@ static ChiakiErrorCode senkusha_send_data_wait_for_ack(ChiakiSenkusha *senkusha, return err; } - diff --git a/lib/src/session.c b/lib/src/session.c index a57ea1d..32cb646 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -540,10 +540,8 @@ quit: #undef QUIT } - - - -typedef struct session_response_t { +typedef struct session_response_t +{ uint32_t error_code; const char *nonce; const char *rp_version; @@ -652,7 +650,6 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch break; } - if(CHIAKI_SOCKET_IS_INVALID(session_sock)) { CHIAKI_LOGE(session->log, "Session request connect failed eventually."); diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index dd069ef..7ad2d2a 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -27,9 +27,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_init(ChiakiStopPipe *stop_pipe) int addr_size = sizeof(stop_pipe->addr); stop_pipe->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if(stop_pipe->fd < 0){ + if(stop_pipe->fd < 0) return CHIAKI_ERR_UNKNOWN; - } stop_pipe->addr.sin_family = AF_INET; // bind to localhost stop_pipe->addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index 28b22dc..2a7bb6f 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -56,7 +56,6 @@ static ChiakiErrorCode stream_connection_send_streaminfo_ack(ChiakiStreamConnect static void stream_connection_takion_av(ChiakiStreamConnection *stream_connection, ChiakiTakionAVPacket *packet); static ChiakiErrorCode stream_connection_send_heartbeat(ChiakiStreamConnection *stream_connection); - CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnection *stream_connection, ChiakiSession *session) { stream_connection->session = session; @@ -121,7 +120,6 @@ CHIAKI_EXPORT void chiaki_stream_connection_fini(ChiakiStreamConnection *stream_ chiaki_mutex_fini(&stream_connection->state_mutex); } - static bool state_finished_cond_check(void *user) { ChiakiStreamConnection *stream_connection = user; @@ -399,7 +397,6 @@ static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *strea break; } chiaki_mutex_unlock(&stream_connection->state_mutex); - } static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) @@ -640,7 +637,6 @@ static bool pb_decode_resolution(pb_istream_t *stream, const pb_field_t *field, return true; } - static void stream_connection_takion_data_expect_streaminfo(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) { tkproto_TakionMessage msg; @@ -709,11 +705,9 @@ error: chiaki_cond_signal(&stream_connection->state_cond); } - - static bool chiaki_pb_encode_zero_encrypted_key(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) { - if (!pb_encode_tag_for_field(stream, field)) + if(!pb_encode_tag_for_field(stream, field)) return false; uint8_t data[] = { 0, 0, 0, 0 }; return pb_encode_string(stream, data, sizeof(data)); @@ -867,7 +861,6 @@ static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection return err; } - static void stream_connection_takion_av(ChiakiStreamConnection *stream_connection, ChiakiTakionAVPacket *packet) { chiaki_gkcrypt_decrypt(stream_connection->gkcrypt_remote, packet->key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, packet->data, packet->data_size); @@ -878,7 +871,6 @@ static void stream_connection_takion_av(ChiakiStreamConnection *stream_connectio chiaki_audio_receiver_av_packet(stream_connection->audio_receiver, packet); } - static ChiakiErrorCode stream_connection_send_heartbeat(ChiakiStreamConnection *stream_connection) { tkproto_TakionMessage msg = { 0 }; diff --git a/lib/src/takion.c b/lib/src/takion.c index fe3e95b..c433977 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -107,7 +107,6 @@ typedef enum takion_chunk_type_t { TAKION_CHUNK_TYPE_COOKIE_ACK = 0xb, } TakionChunkType; - typedef struct takion_message_t { uint32_t tag; @@ -120,7 +119,6 @@ typedef struct takion_message_t uint8_t *payload; } TakionMessage; - typedef struct takion_message_payload_init_t { uint32_t tag; @@ -152,14 +150,12 @@ typedef struct uint16_t channel; } TakionDataPacketEntry; - typedef struct chiaki_takion_postponed_packet_t { uint8_t *buf; size_t buf_size; } ChiakiTakionPostponedPacket; - static void *takion_thread_func(void *user); static void takion_handle_packet(ChiakiTakion *takion, uint8_t *buf, size_t buf_size); static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size); @@ -507,7 +503,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_congestion(ChiakiTakion *takion return err; uint8_t buf[CHIAKI_TAKION_CONGESTION_PACKET_SIZE]; - chiaki_takion_format_congestion(buf, packet, key_pos); + chiaki_takion_format_congestion(buf, packet, key_pos); return chiaki_takion_send(takion, buf, sizeof(buf), key_pos); } @@ -603,7 +599,6 @@ static ChiakiErrorCode takion_handshake(ChiakiTakion *takion, uint32_t *seq_num_ CHIAKI_LOGI(takion->log, "Takion sent init"); - // INIT_ACK <- TakionMessagePayloadInitAck init_ack_payload; @@ -621,20 +616,17 @@ static ChiakiErrorCode takion_handshake(ChiakiTakion *takion, uint32_t *seq_num_ } CHIAKI_LOGI(takion->log, "Takion received init ack with remote tag %#x, outbound streams: %#x, inbound streams: %#x", - init_ack_payload.tag, init_ack_payload.outbound_streams, init_ack_payload.inbound_streams); + init_ack_payload.tag, init_ack_payload.outbound_streams, init_ack_payload.inbound_streams); takion->tag_remote = init_ack_payload.tag; *seq_num_remote_initial = takion->tag_remote; //init_ack_payload.initial_seq_num; - if(init_ack_payload.outbound_streams == 0 || init_ack_payload.inbound_streams == 0 - || init_ack_payload.outbound_streams > TAKION_INBOUND_STREAMS - || init_ack_payload.inbound_streams < TAKION_OUTBOUND_STREAMS) + if(init_ack_payload.outbound_streams == 0 || init_ack_payload.inbound_streams == 0 || init_ack_payload.outbound_streams > TAKION_INBOUND_STREAMS || init_ack_payload.inbound_streams < TAKION_OUTBOUND_STREAMS) { CHIAKI_LOGE(takion->log, "Takion min/max check failed"); return CHIAKI_ERR_INVALID_RESPONSE; } - // COOKIE -> err = takion_send_message_cookie(takion, init_ack_payload.cookie); @@ -780,7 +772,6 @@ beach: return NULL; } - static ChiakiErrorCode takion_recv(ChiakiTakion *takion, uint8_t *buf, size_t *buf_size, uint64_t timeout_ms) { ChiakiErrorCode err = chiaki_stop_pipe_select_single(&takion->stop_pipe, takion->sock, false, timeout_ms); @@ -805,7 +796,6 @@ static ChiakiErrorCode takion_recv(ChiakiTakion *takion, uint8_t *buf, size_t *b return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size) { if(!takion->gkcrypt_remote) @@ -866,7 +856,6 @@ static void takion_postpone_packet(ChiakiTakion *takion, uint8_t *buf, size_t bu packet->buf_size = buf_size; } - /** * @param buf ownership of this buf is taken. */ @@ -934,7 +923,6 @@ static void takion_handle_packet_message(ChiakiTakion *takion, uint8_t *buf, siz } } - static void takion_flush_data_queue(ChiakiTakion *takion) { uint64_t seq_num = 0; @@ -1104,7 +1092,6 @@ static ChiakiErrorCode takion_parse_message(ChiakiTakion *takion, uint8_t *buf, return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_send_message_init(ChiakiTakion *takion, TakionMessagePayloadInit *payload) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + 0x10]; @@ -1121,8 +1108,6 @@ static ChiakiErrorCode takion_send_message_init(ChiakiTakion *takion, TakionMess return chiaki_takion_send_raw(takion, message, sizeof(message)); } - - static ChiakiErrorCode takion_send_message_cookie(ChiakiTakion *takion, uint8_t *cookie) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + TAKION_COOKIE_SIZE]; @@ -1132,8 +1117,6 @@ static ChiakiErrorCode takion_send_message_cookie(ChiakiTakion *takion, uint8_t return chiaki_takion_send_raw(takion, message, sizeof(message)); } - - static ChiakiErrorCode takion_recv_message_init_ack(ChiakiTakion *takion, TakionMessagePayloadInitAck *payload) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + 0x10 + TAKION_COOKIE_SIZE]; @@ -1181,7 +1164,6 @@ static ChiakiErrorCode takion_recv_message_init_ack(ChiakiTakion *takion, Takion return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_recv_message_cookie_ack(ChiakiTakion *takion) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE]; @@ -1221,7 +1203,6 @@ static ChiakiErrorCode takion_recv_message_cookie_ack(ChiakiTakion *takion) return CHIAKI_ERR_SUCCESS; } - static void takion_handle_packet_av(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size) { // HHIxIIx @@ -1453,5 +1434,4 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPac packet->data_size = buf_size; return CHIAKI_ERR_SUCCESS; - } diff --git a/lib/src/thread.c b/lib/src/thread.c index 1948311..0539e16 100644 --- a/lib/src/thread.c +++ b/lib/src/thread.c @@ -23,7 +23,8 @@ static DWORD WINAPI win32_thread_func(LPVOID param) #endif #ifdef __SWITCH__ -int64_t get_thread_limit(){ +int64_t get_thread_limit() +{ uint64_t resource_limit_handle_value = INVALID_HANDLE; svcGetInfo(&resource_limit_handle_value, InfoType_ResourceLimit, INVALID_HANDLE, 0); int64_t thread_cur_value = 0, thread_lim_value = 0; @@ -45,9 +46,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_thread_create(ChiakiThread *thread, ChiakiT return CHIAKI_ERR_THREAD; #else #ifdef __SWITCH__ - if(get_thread_limit() <= 1){ + if(get_thread_limit() <= 1) return CHIAKI_ERR_THREAD; - } #endif int r = pthread_create(&thread->thread, NULL, func, arg); if(r != 0) @@ -90,13 +90,13 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_thread_set_name(ChiakiThread *thread, const if(r != 0) return CHIAKI_ERR_THREAD; #else - (void)thread; (void)name; + (void)thread; + (void)name; #endif #endif return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_mutex_init(ChiakiMutex *mutex, bool rec) { #if _WIN32 @@ -172,9 +172,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_mutex_unlock(ChiakiMutex *mutex) return CHIAKI_ERR_SUCCESS; } - - - CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_init(ChiakiCond *cond) { #if _WIN32 @@ -214,8 +211,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_fini(ChiakiCond *cond) return CHIAKI_ERR_SUCCESS; } - - CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_wait(ChiakiCond *cond, ChiakiMutex *mutex) { #if _WIN32 @@ -323,7 +318,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_timedwait_pred(ChiakiCond *cond, Chiak #endif } return CHIAKI_ERR_SUCCESS; - } CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_signal(ChiakiCond *cond) @@ -350,9 +344,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_broadcast(ChiakiCond *cond) return CHIAKI_ERR_SUCCESS; } - - - CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_init(ChiakiBoolPredCond *cond) { cond->pred = false; @@ -384,7 +375,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_fini(ChiakiBoolPredCond *con return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_lock(ChiakiBoolPredCond *cond) { return chiaki_mutex_lock(&cond->mutex); diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index 7f71578..e9abe5f 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -16,9 +16,9 @@ #define HOSTS_MAX 16 #define DROP_PINGS 3 -static void Discovery(ChiakiDiscoveryHost * discovered_hosts, size_t hosts_count, void * user) +static void Discovery(ChiakiDiscoveryHost *discovered_hosts, size_t hosts_count, void *user) { - DiscoveryManager * dm = (DiscoveryManager *)user; + DiscoveryManager *dm = (DiscoveryManager *)user; for(size_t i = 0; i < hosts_count; i++) { dm->DiscoveryCB(discovered_hosts + i); @@ -105,7 +105,7 @@ uint32_t DiscoveryManager::GetIPv4BroadcastAddr() #endif } -int DiscoveryManager::Send(struct sockaddr * host_addr, size_t host_addr_len) +int DiscoveryManager::Send(struct sockaddr *host_addr, size_t host_addr_len) { if(!host_addr) { @@ -120,9 +120,9 @@ int DiscoveryManager::Send(struct sockaddr * host_addr, size_t host_addr_len) return 0; } -int DiscoveryManager::Send(const char * discover_ip_dest) +int DiscoveryManager::Send(const char *discover_ip_dest) { - struct addrinfo * host_addrinfos; + struct addrinfo *host_addrinfos; int r = getaddrinfo(discover_ip_dest, NULL, NULL, &host_addrinfos); if(r != 0) { @@ -130,10 +130,10 @@ int DiscoveryManager::Send(const char * discover_ip_dest) return 1; } - struct sockaddr * host_addr = nullptr; + struct sockaddr *host_addr = nullptr; socklen_t host_addr_len = 0; - for(struct addrinfo * ai = host_addrinfos; ai; ai = ai->ai_next) + for(struct addrinfo *ai = host_addrinfos; ai; ai = ai->ai_next) { if(ai->ai_protocol != IPPROTO_UDP) continue; @@ -170,13 +170,13 @@ int DiscoveryManager::Send() return DiscoveryManager::Send(this->host_addr, this->host_addr_len); } -void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) +void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost *discovered_host) { // the user ptr is passed as // chiaki_discovery_thread_start arg std::string key = discovered_host->host_name; - Host * host = this->settings->GetOrCreateHost(&key); + Host *host = this->settings->GetOrCreateHost(&key); CHIAKI_LOGI(this->log, "--"); CHIAKI_LOGI(this->log, "Discovered Host:"); diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index e49852b..d784026 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -100,7 +100,7 @@ void HostInterface::Register(Host *host, std::function success_cb) brls::Dialog *peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View *view) { bool pin_provided = false; - char pin_input[9] = {0}; + char pin_input[9] = { 0 }; std::string error_message; // use callback to ensure that the message is showed on screen @@ -330,7 +330,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) brls::ListItem *psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); psn_account_id->setValue(psn_account_id_string.c_str()); auto psn_account_id_cb = [this, host, psn_account_id](brls::View *view) { - char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = {0}; + char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = { 0 }; bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); if(input) { @@ -349,7 +349,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) brls::ListItem *psn_online_id = new brls::ListItem("PSN Online ID"); psn_online_id->setValue(psn_online_id_string.c_str()); auto psn_online_id_cb = [this, host, psn_online_id](brls::View *view) { - char online_id[256] = {0}; + char online_id[256] = { 0 }; bool input = this->io->ReadUserKeyboard(online_id, sizeof(online_id)); if(input) { @@ -380,7 +380,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) } brls::SelectListItem *resolution = new brls::SelectListItem( - "Resolution", {"720p", "540p", "360p"}, value); + "Resolution", { "720p", "540p", "360p" }, value); auto resolution_cb = [this, host](int result) { ChiakiVideoResolutionPreset value = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; @@ -414,7 +414,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) } brls::SelectListItem *fps = new brls::SelectListItem( - "FPS", {"60", "30"}, value); + "FPS", { "60", "30" }, value); auto fps_cb = [this, host](int result) { ChiakiVideoFPSPreset value = CHIAKI_VIDEO_FPS_PRESET_60; @@ -467,7 +467,7 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) brls::ListItem *display_name = new brls::ListItem("Display name"); auto display_name_cb = [this, display_name](brls::View *view) { - char name[16] = {0}; + char name[16] = { 0 }; bool input = this->io->ReadUserKeyboard(name, sizeof(name)); if(input) { @@ -482,7 +482,7 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) brls::ListItem *address = new brls::ListItem("Remote IP/name"); auto address_cb = [this, address](brls::View *view) { - char addr[256] = {0}; + char addr[256] = { 0 }; bool input = this->io->ReadUserKeyboard(addr, sizeof(addr)); if(input) { @@ -500,7 +500,7 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::ListItem* port = new brls::ListItem("Remote stream port", "udp 9296"); // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); brls::SelectListItem *ps_version = new brls::SelectListItem("PlayStation Version", - {"PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); + { "PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7" }); auto ps_version_cb = [this, ps_version](int result) { switch(result) { diff --git a/switch/src/io.cpp b/switch/src/io.cpp index d7c28f7..9f6ab56 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -646,7 +646,7 @@ bool IO::InitOpenGlTextures() D(glGenTextures(PLANES_COUNT, this->tex)); D(glGenBuffers(PLANES_COUNT, this->pbo)); - uint8_t uv_default[] = {0x7f, 0x7f}; + uint8_t uv_default[] = { 0x7f, 0x7f }; for(int i = 0; i < PLANES_COUNT; i++) { D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); @@ -659,7 +659,7 @@ bool IO::InitOpenGlTextures() D(glUseProgram(this->prog)); // bind only as many planes as we need - const char *plane_names[] = {"plane1", "plane2", "plane3"}; + const char *plane_names[] = { "plane1", "plane2", "plane3" }; for(int i = 0; i < PLANES_COUNT; i++) D(glUniform1i(glGetUniformLocation(this->prog, plane_names[i]), i)); diff --git a/switch/src/main.cpp b/switch/src/main.cpp index c12d413..84f9210 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -111,11 +111,11 @@ extern "C" void userAppExit() } #endif // __SWITCH__ -int main(int argc, char * argv[]) +int main(int argc, char *argv[]) { // load chiaki lib - Settings * settings = Settings::GetInstance(); - ChiakiLog * log = settings->GetLogger(); + Settings *settings = Settings::GetInstance(); + ChiakiLog *log = settings->GetLogger(); CHIAKI_LOGI(log, "Loading chaki lib"); diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index f0eb3e3..f5edf1f 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -13,7 +13,7 @@ Settings::Settings() #endif } -Settings::ConfigurationItem Settings::ParseLine(std::string * line, std::string * value) +Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string *value) { Settings::ConfigurationItem ci; std::smatch m; @@ -35,9 +35,9 @@ size_t Settings::GetB64encodeSize(size_t in) return ((4 * in / 3) + 3) & ~3; } -Settings * Settings::instance = nullptr; +Settings *Settings::instance = nullptr; -Settings * Settings::GetInstance() +Settings *Settings::GetInstance() { if(instance == nullptr) { @@ -47,17 +47,17 @@ Settings * Settings::GetInstance() return instance; } -ChiakiLog * Settings::GetLogger() +ChiakiLog *Settings::GetLogger() { return &this->log; } -std::map * Settings::GetHostsMap() +std::map *Settings::GetHostsMap() { return &this->hosts; } -Host * Settings::GetOrCreateHost(std::string * host_name) +Host *Settings::GetOrCreateHost(std::string *host_name) { bool created = false; // update of create Host instance @@ -69,7 +69,7 @@ Host * Settings::GetOrCreateHost(std::string * host_name) created = true; } - Host * host = &(this->hosts.at(*host_name)); + Host *host = &(this->hosts.at(*host_name)); if(created) { // copy default settings @@ -90,7 +90,7 @@ void Settings::ParseFile() std::string line; std::string value; bool rp_key_b = false, rp_regist_key_b = false, rp_key_type_b = false; - Host * current_host = nullptr; + Host *current_host = nullptr; if(config_file.is_open()) { CHIAKI_LOGV(&this->log, "Config file opened"); @@ -102,63 +102,63 @@ void Settings::ParseFile() ci = this->ParseLine(&line, &value); switch(ci) { - // got to next line - case UNKNOWN: - CHIAKI_LOGV(&this->log, "UNKNOWN config"); - break; - case HOST_NAME: - CHIAKI_LOGV(&this->log, "HOST_NAME %s", value.c_str()); - // current host is in context - current_host = this->GetOrCreateHost(&value); - // all following case will edit the current_host config + // got to next line + case UNKNOWN: + CHIAKI_LOGV(&this->log, "UNKNOWN config"); + break; + case HOST_NAME: + CHIAKI_LOGV(&this->log, "HOST_NAME %s", value.c_str()); + // current host is in context + current_host = this->GetOrCreateHost(&value); + // all following case will edit the current_host config - rp_key_b = false; - rp_regist_key_b = false; - rp_key_type_b = false; - break; - case HOST_ADDR: - CHIAKI_LOGV(&this->log, "HOST_ADDR %s", value.c_str()); - if(current_host != nullptr) - current_host->host_addr = value; - break; - case PSN_ONLINE_ID: - CHIAKI_LOGV(&this->log, "PSN_ONLINE_ID %s", value.c_str()); - // current_host == nullptr - // means we are in global ini section - // update default setting - this->SetPSNOnlineID(current_host, value); - break; - case PSN_ACCOUNT_ID: - CHIAKI_LOGV(&this->log, "PSN_ACCOUNT_ID %s", value.c_str()); - this->SetPSNAccountID(current_host, value); - break; - case RP_KEY: - CHIAKI_LOGV(&this->log, "RP_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_key_b = this->SetHostRPKey(current_host, value); - break; - case RP_KEY_TYPE: - CHIAKI_LOGV(&this->log, "RP_KEY_TYPE %s", value.c_str()); - if(current_host != nullptr) - // TODO Check possible rp_type values - rp_key_type_b = this->SetHostRPKeyType(current_host, value); - break; - case RP_REGIST_KEY: - CHIAKI_LOGV(&this->log, "RP_REGIST_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); - break; - case VIDEO_RESOLUTION: - this->SetVideoResolution(current_host, value); - break; - case VIDEO_FPS: - this->SetVideoFPS(current_host, value); - break; - case TARGET: - CHIAKI_LOGV(&this->log, "TARGET %s", value.c_str()); - if(current_host != nullptr) - this->SetChiakiTarget(current_host, value); - break; + rp_key_b = false; + rp_regist_key_b = false; + rp_key_type_b = false; + break; + case HOST_ADDR: + CHIAKI_LOGV(&this->log, "HOST_ADDR %s", value.c_str()); + if(current_host != nullptr) + current_host->host_addr = value; + break; + case PSN_ONLINE_ID: + CHIAKI_LOGV(&this->log, "PSN_ONLINE_ID %s", value.c_str()); + // current_host == nullptr + // means we are in global ini section + // update default setting + this->SetPSNOnlineID(current_host, value); + break; + case PSN_ACCOUNT_ID: + CHIAKI_LOGV(&this->log, "PSN_ACCOUNT_ID %s", value.c_str()); + this->SetPSNAccountID(current_host, value); + break; + case RP_KEY: + CHIAKI_LOGV(&this->log, "RP_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_key_b = this->SetHostRPKey(current_host, value); + break; + case RP_KEY_TYPE: + CHIAKI_LOGV(&this->log, "RP_KEY_TYPE %s", value.c_str()); + if(current_host != nullptr) + // TODO Check possible rp_type values + rp_key_type_b = this->SetHostRPKeyType(current_host, value); + break; + case RP_REGIST_KEY: + CHIAKI_LOGV(&this->log, "RP_REGIST_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); + break; + case VIDEO_RESOLUTION: + this->SetVideoResolution(current_host, value); + break; + case VIDEO_FPS: + this->SetVideoFPS(current_host, value); + break; + case TARGET: + CHIAKI_LOGV(&this->log, "TARGET %s", value.c_str()); + if(current_host != nullptr) + this->SetChiakiTarget(current_host, value); + break; } // ci switch if(rp_key_b && rp_regist_key_b && rp_key_type_b) // the current host contains rp key data @@ -231,7 +231,7 @@ int Settings::WriteFile() if(it->second.rp_key_data || it->second.registered) { - char rp_key_type[33] = {0}; + char rp_key_type[33] = { 0 }; snprintf(rp_key_type, sizeof(rp_key_type), "%d", it->second.rp_key_type); // save registered rp key for auto login config_file << "rp_key = \"" << this->GetHostRPKey(&it->second) << "\"\n" @@ -250,14 +250,14 @@ std::string Settings::ResolutionPresetToString(ChiakiVideoResolutionPreset resol { switch(resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return "360p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return "540p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return "720p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return "1080p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return "360p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return "540p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return "720p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return "1080p"; } return "UNKNOWN"; } @@ -266,14 +266,14 @@ int Settings::ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution) { switch(resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return 360; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return 540; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return 720; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return 1080; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return 360; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return 540; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return 720; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return 1080; } return 0; } @@ -300,10 +300,10 @@ std::string Settings::FPSPresetToString(ChiakiVideoFPSPreset fps) { switch(fps) { - case CHIAKI_VIDEO_FPS_PRESET_30: - return "30"; - case CHIAKI_VIDEO_FPS_PRESET_60: - return "60"; + case CHIAKI_VIDEO_FPS_PRESET_30: + return "30"; + case CHIAKI_VIDEO_FPS_PRESET_60: + return "60"; } return "UNKNOWN"; } @@ -312,10 +312,10 @@ int Settings::FPSPresetToInt(ChiakiVideoFPSPreset fps) { switch(fps) { - case CHIAKI_VIDEO_FPS_PRESET_30: - return 30; - case CHIAKI_VIDEO_FPS_PRESET_60: - return 60; + case CHIAKI_VIDEO_FPS_PRESET_30: + return 30; + case CHIAKI_VIDEO_FPS_PRESET_60: + return 60; } return 0; } @@ -334,7 +334,7 @@ ChiakiVideoFPSPreset Settings::StringToFPSPreset(std::string value) return CHIAKI_VIDEO_FPS_PRESET_30; } -std::string Settings::GetHostName(Host * host) +std::string Settings::GetHostName(Host *host) { if(host != nullptr) return host->GetHostName(); @@ -343,7 +343,7 @@ std::string Settings::GetHostName(Host * host) return ""; } -std::string Settings::GetHostAddr(Host * host) +std::string Settings::GetHostAddr(Host *host) { if(host != nullptr) return host->GetHostAddr(); @@ -352,7 +352,7 @@ std::string Settings::GetHostAddr(Host * host) return ""; } -std::string Settings::GetPSNOnlineID(Host * host) +std::string Settings::GetPSNOnlineID(Host *host) { if(host == nullptr || host->psn_online_id.length() == 0) return this->global_psn_online_id; @@ -360,7 +360,7 @@ std::string Settings::GetPSNOnlineID(Host * host) return host->psn_online_id; } -void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) +void Settings::SetPSNOnlineID(Host *host, std::string psn_online_id) { if(host == nullptr) this->global_psn_online_id = psn_online_id; @@ -368,7 +368,7 @@ void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) host->psn_online_id = psn_online_id; } -std::string Settings::GetPSNAccountID(Host * host) +std::string Settings::GetPSNAccountID(Host *host) { if(host == nullptr || host->psn_account_id.length() == 0) return this->global_psn_account_id; @@ -376,7 +376,7 @@ std::string Settings::GetPSNAccountID(Host * host) return host->psn_account_id; } -void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) +void Settings::SetPSNAccountID(Host *host, std::string psn_account_id) { if(host == nullptr) this->global_psn_account_id = psn_account_id; @@ -384,7 +384,7 @@ void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) host->psn_account_id = psn_account_id; } -ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) +ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host *host) { if(host == nullptr) return this->global_video_resolution; @@ -392,7 +392,7 @@ ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) return host->video_resolution; } -void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value) +void Settings::SetVideoResolution(Host *host, ChiakiVideoResolutionPreset value) { if(host == nullptr) this->global_video_resolution = value; @@ -400,13 +400,13 @@ void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value host->video_resolution = value; } -void Settings::SetVideoResolution(Host * host, std::string value) +void Settings::SetVideoResolution(Host *host, std::string value) { ChiakiVideoResolutionPreset p = StringToResolutionPreset(value); this->SetVideoResolution(host, p); } -ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) +ChiakiVideoFPSPreset Settings::GetVideoFPS(Host *host) { if(host == nullptr) return this->global_video_fps; @@ -414,7 +414,7 @@ ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) return host->video_fps; } -void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) +void Settings::SetVideoFPS(Host *host, ChiakiVideoFPSPreset value) { if(host == nullptr) this->global_video_fps = value; @@ -422,18 +422,18 @@ void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) host->video_fps = value; } -void Settings::SetVideoFPS(Host * host, std::string value) +void Settings::SetVideoFPS(Host *host, std::string value) { ChiakiVideoFPSPreset p = StringToFPSPreset(value); this->SetVideoFPS(host, p); } -ChiakiTarget Settings::GetChiakiTarget(Host * host) +ChiakiTarget Settings::GetChiakiTarget(Host *host) { return host->GetChiakiTarget(); } -bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) +bool Settings::SetChiakiTarget(Host *host, ChiakiTarget target) { if(host != nullptr) { @@ -447,20 +447,20 @@ bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) } } -bool Settings::SetChiakiTarget(Host * host, std::string value) +bool Settings::SetChiakiTarget(Host *host, std::string value) { // TODO Check possible target values return this->SetChiakiTarget(host, static_cast(std::atoi(value.c_str()))); } -std::string Settings::GetHostRPKey(Host * host) +std::string Settings::GetHostRPKey(Host *host) { if(host != nullptr) { if(host->rp_key_data || host->registered) { size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); - char rp_key_b64[rp_key_b64_sz + 1] = {0}; + char rp_key_b64[rp_key_b64_sz + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( host->rp_key, 0x10, @@ -478,7 +478,7 @@ std::string Settings::GetHostRPKey(Host * host) return ""; } -bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) +bool Settings::SetHostRPKey(Host *host, std::string rp_key_b64) { if(host != nullptr) { @@ -497,14 +497,14 @@ bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) return false; } -std::string Settings::GetHostRPRegistKey(Host * host) +std::string Settings::GetHostRPRegistKey(Host *host) { if(host != nullptr) { if(host->rp_key_data || host->registered) { size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); - char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = {0}; + char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( (uint8_t *)host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, @@ -522,7 +522,7 @@ std::string Settings::GetHostRPRegistKey(Host * host) return ""; } -bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) +bool Settings::SetHostRPRegistKey(Host *host, std::string rp_regist_key_b64) { if(host != nullptr) { @@ -541,7 +541,7 @@ bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) return false; } -int Settings::GetHostRPKeyType(Host * host) +int Settings::GetHostRPKeyType(Host *host) { if(host != nullptr) return host->rp_key_type; @@ -550,7 +550,7 @@ int Settings::GetHostRPKeyType(Host * host) return 0; } -bool Settings::SetHostRPKeyType(Host * host, std::string value) +bool Settings::SetHostRPKeyType(Host *host, std::string value) { if(host != nullptr) { @@ -562,7 +562,7 @@ bool Settings::SetHostRPKeyType(Host * host, std::string value) } #ifdef CHIAKI_ENABLE_SWITCH_OVERCLOCK -int Settings::GetCPUOverclock(Host * host) +int Settings::GetCPUOverclock(Host *host) { if(host == nullptr) return this->global_cpu_overclock; @@ -570,7 +570,7 @@ int Settings::GetCPUOverclock(Host * host) return host->cpu_overclock; } -void Settings::SetCPUOverclock(Host * host, int value) +void Settings::SetCPUOverclock(Host *host, int value) { int oc = OC_1326; if(value > OC_1580) @@ -592,11 +592,9 @@ void Settings::SetCPUOverclock(Host * host, int value) host->cpu_overclock = oc; } -void Settings::SetCPUOverclock(Host * host, std::string value) +void Settings::SetCPUOverclock(Host *host, std::string value) { int v = atoi(value.c_str()); this->SetCPUOverclock(host, v); } #endif - - From da051803f59dab6aa31bef0a2f4f22eaf1679402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 21:19:52 +0100 Subject: [PATCH 009/104] Fall back to H264 on Pi --- gui/src/streamsession.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index b1cbb47..01008a8 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -108,6 +108,14 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_connect_info.video_profile = connect_info.video_profile; chiaki_connect_info.video_profile_auto_downgrade = true; +#if CHIAKI_LIB_ENABLE_PI_DECODER + if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264) + { + CHIAKI_LOGW(GetChiakiLog(), "A codec other than H264 was requested for Pi Decoder. Falling back to it."); + chiaki_connect_info.video_profile.codec = CHIAKI_CODEC_H264; + } +#endif + if(connect_info.regist_key.size() != sizeof(chiaki_connect_info.regist_key)) throw ChiakiException("RegistKey invalid"); memcpy(chiaki_connect_info.regist_key, connect_info.regist_key.constData(), sizeof(chiaki_connect_info.regist_key)); From a2955d21fc565fc85e5570e6ff9d742bbd387204 Mon Sep 17 00:00:00 2001 From: Alexander Millin Date: Fri, 1 Jan 2021 21:23:19 +0100 Subject: [PATCH 010/104] Enable hevc_vaapi in FFMPEG Builds --- scripts/build-ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index 4761520..715a87f 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -9,6 +9,6 @@ TAG=n4.3.1 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 -./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 +./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 make -j4 || exit 1 make install || exit 1 From d4dc0ffee19ccc29764dff2f8e298a632eab2c7c Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Sat, 2 Jan 2021 13:23:14 +0100 Subject: [PATCH 011/104] Fix switch README info and nxlink push script --- scripts/switch/push-docker-build-chiaki.sh | 10 ++++++---- switch/README.md | 20 ++------------------ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/scripts/switch/push-docker-build-chiaki.sh b/scripts/switch/push-docker-build-chiaki.sh index ff66e40..ac82029 100755 --- a/scripts/switch/push-docker-build-chiaki.sh +++ b/scripts/switch/push-docker-build-chiaki.sh @@ -3,8 +3,10 @@ cd "`dirname $(readlink -f ${0})`/../.." docker run \ - -v "`pwd`:/build/chiaki" \ - -p 28771:28771 -ti \ - chiaki-switch \ - -c "/opt/devkitpro/tools/bin/nxlink -a $@ -s /build/chiaki/build_switch/switch/chiaki.nro" + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + -ti -p 28771:28771 \ + --entrypoint /opt/devkitpro/tools/bin/nxlink \ + thestr4ng3r/chiaki-build-switch \ + "$@" -s /build/chiaki/build_switch/switch/chiaki.nro diff --git a/switch/README.md b/switch/README.md index 709f900..f5fbe68 100644 --- a/switch/README.md +++ b/switch/README.md @@ -4,22 +4,6 @@ this project requires the devkitpro toolchain. you can use your personal computer to install devkitpro but the easiest way is to use the following container. -Build container image ---------------------- -``` -bash scripts/switch/build-docker-image.sh -``` - -Run container -------------- -from the project's [root folder](../) -``` -docker run -it --rm \ - -v "$(pwd):/build" \ - -p 28771:28771 \ - chiaki-switch -``` - Build Project ------------- ``` @@ -31,7 +15,7 @@ tools Push to homebrew Netloader ``` # where X.X.X.X is the IP of your switch -scripts/switch/push-docker-build-chiaki.sh 192.168.0.200 +bash scripts/switch/push-docker-build-chiaki.sh -a 192.168.0.200 ``` Troubleshoot @@ -52,7 +36,7 @@ this file contains sensitive data. (do not share this file) [PS*-***] # required: lan PlayStation IP address # IP from Settings > System > system information -host_ip = *.*.*.* +host_addr = *.*.*.* # required: sony oline id (login) psn_online_id = ps_online_id # required (PS4>7.0 Only): https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#obtaining-your-psn-accountid From 7cf370c70dc4680af06acb3008f8625b0ff1fe75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 4 Jan 2021 11:16:28 +0100 Subject: [PATCH 012/104] Show SDL GUID in Controller Name in GUI --- gui/src/controllermanager.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index a78fd44..667b736 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -251,7 +251,11 @@ QString Controller::GetName() #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(!controller) return QString(); - return SDL_GameControllerName(controller); + SDL_Joystick *js = SDL_GameControllerGetJoystick(controller); + SDL_JoystickGUID guid = SDL_JoystickGetGUID(js); + char guid_str[256]; + SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); + return QString("%1 (%2)").arg(SDL_JoystickName(js), guid_str); #else return QString(); #endif From e531a90d2670590b493244b7bf1d07088e4b194d Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Sat, 2 Jan 2021 13:45:56 +0100 Subject: [PATCH 013/104] Fix switch settings psn id regex --- switch/include/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switch/include/settings.h b/switch/include/settings.h index 08ecbb2..fbf43b8 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -50,7 +50,7 @@ class Settings const std::map re_map = { {HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]")}, {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?")}, - {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?(\\w+)\"?")}, + {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?([\\w_-]+)\"?")}, {PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY_TYPE, std::regex("^\\s*rp_key_type\\s*=\\s*\"?(\\d)\"?")}, From b9a9ea497c970f6dd6a0b210adca96f3f288091e Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 4 Jan 2021 11:08:57 +0100 Subject: [PATCH 014/104] Fix switch audio delay with SDL_ClearQueuedAudio --- switch/src/io.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 9f6ab56..3104bd4 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -291,6 +291,16 @@ void IO::AudioCB(int16_t *buf, size_t samples_count) else buf[x] = (int16_t)sample; } + + int audio_queued_size = SDL_GetQueuedAudioSize(this->sdl_audio_device_id); + if(audio_queued_size > 16000) + { + // clear audio queue to avoid big audio delay + // average values are close to 13000 bytes + CHIAKI_LOGW(this->log, "Triggering SDL_ClearQueuedAudio with queue size = %d", audio_queued_size); + SDL_ClearQueuedAudio(this->sdl_audio_device_id); + } + int success = SDL_QueueAudio(this->sdl_audio_device_id, buf, sizeof(int16_t) * samples_count * 2); if(success != 0) CHIAKI_LOGE(this->log, "SDL_QueueAudio failed: %s\n", SDL_GetError()); From 1ee23e0fa2c5f514acc9e2ae19b15a055e47628e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 4 Jan 2021 12:44:13 +0100 Subject: [PATCH 015/104] Don't mess with the OMX Buffer Size --- lib/src/pidecoder.c | 105 ++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/lib/src/pidecoder.c b/lib/src/pidecoder.c index 72a22f5..ae99fbd 100644 --- a/lib/src/pidecoder.c +++ b/lib/src/pidecoder.c @@ -8,7 +8,6 @@ #include #include -#define MAX_DECODE_UNIT_SIZE 262144 CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, ChiakiLog *log) { @@ -108,29 +107,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, C return CHIAKI_ERR_UNKNOWN; } - OMX_PARAM_PORTDEFINITIONTYPE port; - - memset(&port, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE)); - port.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); - port.nVersion.nVersion = OMX_VERSION; - port.nPortIndex = 130; - if(OMX_GetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "Failed to get decoder port definition\n"); - chiaki_pi_decoder_fini(decoder); - return CHIAKI_ERR_UNKNOWN; - } - - // Increase the buffer size to fit the largest possible frame - port.nBufferSize = MAX_DECODE_UNIT_SIZE; - - if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for port"); - chiaki_pi_decoder_fini(decoder); - return CHIAKI_ERR_UNKNOWN; - } - if(ilclient_enable_port_buffers(decoder->video_decode, 130, NULL, NULL, NULL) != 0) { CHIAKI_LOGE(decoder->log, "ilclient_enable_port_buffers failed"); @@ -180,50 +156,55 @@ CHIAKI_EXPORT void chiaki_pi_decoder_fini(ChiakiPiDecoder *decoder) static bool push_buffer(ChiakiPiDecoder *decoder, uint8_t *buf, size_t buf_size) { - OMX_BUFFERHEADERTYPE *omx_buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1); - if(!omx_buf) + while(buf_size) { - CHIAKI_LOGE(decoder->log, "ilclient_get_input_buffer failed"); - return false; - } - - if(omx_buf->nAllocLen < buf_size) - { - CHIAKI_LOGE(decoder->log, "Buffer from omx is too small for frame"); - return false; - } - - omx_buf->nFilledLen = 0; - omx_buf->nOffset = 0; - omx_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME; - if(decoder->first_packet) - { - omx_buf->nFlags |= OMX_BUFFERFLAG_STARTTIME; - decoder->first_packet = false; - } - - memcpy(omx_buf->pBuffer + omx_buf->nFilledLen, buf, buf_size); - omx_buf->nFilledLen += buf_size; - - if(!decoder->port_settings_changed - && ((omx_buf->nFilledLen > 0 && ilclient_remove_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1) == 0) - || (omx_buf->nFilledLen == 0 && ilclient_wait_for_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1, ILCLIENT_EVENT_ERROR | ILCLIENT_PARAMETER_CHANGED, 10000) == 0))) - { - decoder->port_settings_changed = true; - - if(ilclient_setup_tunnel(decoder->tunnel, 0, 0) != 0) + OMX_BUFFERHEADERTYPE *omx_buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1); + if(!omx_buf) { - CHIAKI_LOGE(decoder->log, "ilclient_setup_tunnel failed"); + CHIAKI_LOGE(decoder->log, "ilclient_get_input_buffer failed"); return false; } - ilclient_change_component_state(decoder->video_render, OMX_StateExecuting); - } + size_t push_size = buf_size; + if(push_size > omx_buf->nAllocLen) + { + CHIAKI_LOGW(decoder->log, "OMX Buffer too small, fragmenting to multiple buffers."); + push_size = omx_buf->nAllocLen; + } + memcpy(omx_buf->pBuffer, buf, push_size); + buf_size -= push_size; + buf += push_size; + omx_buf->nFilledLen = push_size; + omx_buf->nOffset = 0; + omx_buf->nFlags = 0; + if(!buf_size) + omx_buf->nFlags |= OMX_BUFFERFLAG_ENDOFFRAME; + if(decoder->first_packet) + { + omx_buf->nFlags |= OMX_BUFFERFLAG_STARTTIME; + decoder->first_packet = false; + } - if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), omx_buf) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "OMX_EmptyThisBuffer failed"); - return false; + if(!decoder->port_settings_changed + && ((omx_buf->nFilledLen > 0 && ilclient_remove_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1) == 0) + || (omx_buf->nFilledLen == 0 && ilclient_wait_for_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1, ILCLIENT_EVENT_ERROR | ILCLIENT_PARAMETER_CHANGED, 10000) == 0))) + { + decoder->port_settings_changed = true; + + if(ilclient_setup_tunnel(decoder->tunnel, 0, 0) != 0) + { + CHIAKI_LOGE(decoder->log, "ilclient_setup_tunnel failed"); + return false; + } + + ilclient_change_component_state(decoder->video_render, OMX_StateExecuting); + } + + if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), omx_buf) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_EmptyThisBuffer failed"); + return false; + } } return true; } From 0af3ae27d45345de7bdf8dc0a2b83a8ac2b808cf Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 4 Jan 2021 17:22:50 +0100 Subject: [PATCH 016/104] Use borealis keyboard for the nintendo switch --- switch/borealis | 2 +- switch/include/gui.h | 4 - switch/include/host.h | 2 +- switch/include/io.h | 1 - switch/src/gui.cpp | 203 +++++++++++++++++++----------------------- switch/src/host.cpp | 13 ++- switch/src/io.cpp | 36 -------- 7 files changed, 98 insertions(+), 163 deletions(-) diff --git a/switch/borealis b/switch/borealis index 205e97a..cbdc1b6 160000 --- a/switch/borealis +++ b/switch/borealis @@ -1 +1 @@ -Subproject commit 205e97ab45922fa7f5c9fa6a85d5d686cd50b669 +Subproject commit cbdc1b65314d1eeb2799deae5cf6f113d6d67b46 diff --git a/switch/include/gui.h b/switch/include/gui.h index afd2bab..38f13ec 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -50,10 +50,6 @@ class MainApplication IO *io; brls::TabFrame *rootFrame; std::map host_menuitems; - // add_host local settings - std::string remote_display_name = ""; - std::string remote_addr = ""; - ChiakiTarget remote_ps_version = CHIAKI_TARGET_PS5_1; bool BuildConfigurationMenu(brls::List *, Host *host = nullptr); void BuildAddHostConfigurationMenu(brls::List *); diff --git a/switch/include/host.h b/switch/include/host.h index ae6f05e..a93e7c9 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -90,7 +90,7 @@ class Host public: Host(std::string host_name); ~Host(); - int Register(std::string pin); + int Register(int pin); int Wakeup(); int InitSession(IO *); int FiniSession(); diff --git a/switch/include/io.h b/switch/include/io.h index 8427684..85e2966 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -102,7 +102,6 @@ class IO bool FreeVideo(); bool InitJoystick(); bool FreeJoystick(); - bool ReadUserKeyboard(char *buffer, size_t buffer_size); bool MainLoop(ChiakiControllerState *state); }; diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index d784026..f31cba8 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -97,42 +97,31 @@ void HostInterface::Register(Host *host, std::function success_cb) host->SetRegistEventTypeFinishedFailed(event_type_finished_failed_cb); // the host is not registered yet - brls::Dialog *peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); - brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View *view) { - bool pin_provided = false; - char pin_input[9] = { 0 }; - std::string error_message; - - // use callback to ensure that the message is showed on screen - // before the the ReadUserKeyboard - peprpc->close(); - - pin_provided = io->ReadUserKeyboard(pin_input, sizeof(pin_input)); - if(pin_provided) + // use callback to ensure that the message is showed on screen + // before the Swkbd + auto pin_input_cb = [host](int pin) { + // prevent users form messing with the gui + brls::Application::blockInputs(); + int ret = host->Register(pin); + if(ret != HOST_REGISTER_OK) { - // prevent users form messing with the gui - brls::Application::blockInputs(); - int ret = host->Register(pin_input); - if(ret != HOST_REGISTER_OK) + switch(ret) { - switch(ret) - { - // account not configured - case HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID: - brls::Application::notify("No PSN Account ID provided"); - brls::Application::unblockInputs(); - break; - case HOST_REGISTER_ERROR_SETTING_PSNONLINEID: - brls::Application::notify("No PSN Online ID provided"); - brls::Application::unblockInputs(); - break; - } + // account not configured + case HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID: + brls::Application::notify("No PSN Account ID provided"); + brls::Application::unblockInputs(); + break; + case HOST_REGISTER_ERROR_SETTING_PSNONLINEID: + brls::Application::notify("No PSN Online ID provided"); + brls::Application::unblockInputs(); + break; } } }; - peprpc->addButton("Ok", cb_peprpc); - peprpc->setCancelable(false); - peprpc->open(); + // the pin is 8 digit + bool success = brls::Swkbd::openForNumber(pin_input_cb, + "Please enter your PlayStation registration PIN code", "8 digits without spaces", 8, "", "", ""); } void HostInterface::Register() @@ -327,39 +316,36 @@ bool MainApplication::Load() bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) { std::string psn_account_id_string = this->settings->GetPSNAccountID(host); - brls::ListItem *psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); - psn_account_id->setValue(psn_account_id_string.c_str()); + brls::InputListItem *psn_account_id = new brls::InputListItem("PSN Account ID", psn_account_id_string, + "Account ID in base64 format", "PS5 or PS4 v7.0 and greater", CHIAKI_PSN_ACCOUNT_ID_SIZE * 2, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + auto psn_account_id_cb = [this, host, psn_account_id](brls::View *view) { - char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = { 0 }; - bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); - if(input) - { - // update gui - psn_account_id->setValue(account_id); - // push in setting - this->settings->SetPSNAccountID(host, account_id); - // write on disk - this->settings->WriteFile(); - } + // retrieve, push and save setting + this->settings->SetPSNAccountID(host, psn_account_id->getValue()); + // write on disk + this->settings->WriteFile(); }; psn_account_id->getClickEvent()->subscribe(psn_account_id_cb); ls->addView(psn_account_id); std::string psn_online_id_string = this->settings->GetPSNOnlineID(host); - brls::ListItem *psn_online_id = new brls::ListItem("PSN Online ID"); - psn_online_id->setValue(psn_online_id_string.c_str()); + brls::InputListItem *psn_online_id = new brls::InputListItem("PSN Online ID", + psn_online_id_string, "", "", 16, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + auto psn_online_id_cb = [this, host, psn_online_id](brls::View *view) { - char online_id[256] = { 0 }; - bool input = this->io->ReadUserKeyboard(online_id, sizeof(online_id)); - if(input) - { - // update gui - psn_online_id->setValue(online_id); - // push in setting - this->settings->SetPSNOnlineID(host, online_id); - // write on disk - this->settings->WriteFile(); - } + // retrieve, push and save setting + this->settings->SetPSNOnlineID(host, psn_online_id->getValue()); + // write on disk + this->settings->WriteFile(); }; psn_online_id->getClickEvent()->subscribe(psn_online_id_cb); ls->addView(psn_online_id); @@ -465,34 +451,24 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::Label* add_host_label = new brls::Label(brls::LabelStyle::REGULAR, // "Add Host configuration", true); - brls::ListItem *display_name = new brls::ListItem("Display name"); - auto display_name_cb = [this, display_name](brls::View *view) { - char name[16] = { 0 }; - bool input = this->io->ReadUserKeyboard(name, sizeof(name)); - if(input) - { - // update gui - display_name->setValue(name); - // set internal value - this->remote_display_name = name; - } - }; - display_name->getClickEvent()->subscribe(display_name_cb); + brls::InputListItem *display_name = new brls::InputListItem("Display name", + "default", "configuration name", "", 16, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + add_host->addView(display_name); - brls::ListItem *address = new brls::ListItem("Remote IP/name"); - auto address_cb = [this, address](brls::View *view) { - char addr[256] = { 0 }; - bool input = this->io->ReadUserKeyboard(addr, sizeof(addr)); - if(input) - { - // update gui - address->setValue(addr); - // set internal value - this->remote_addr = addr; - } - }; - address->getClickEvent()->subscribe(address_cb); + brls::InputListItem *address = new brls::InputListItem("Remote IP/name", + "", "IP address or fqdn", "", 255, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + add_host->addView(address); // TODO @@ -501,46 +477,48 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); brls::SelectListItem *ps_version = new brls::SelectListItem("PlayStation Version", { "PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7" }); - auto ps_version_cb = [this, ps_version](int result) { - switch(result) - { - case 0: - // ps5 v1 - this->remote_ps_version = CHIAKI_TARGET_PS5_1; - break; - case 1: - // ps4 v8 - this->remote_ps_version = CHIAKI_TARGET_PS4_10; - break; - case 2: - // ps4 v7 - this->remote_ps_version = CHIAKI_TARGET_PS4_9; - break; - case 3: - // ps4 v6 - this->remote_ps_version = CHIAKI_TARGET_PS4_8; - break; - } - }; - ps_version->getValueSelectedEvent()->subscribe(ps_version_cb); add_host->addView(ps_version); brls::ListItem *register_host = new brls::ListItem("Register"); - auto register_host_cb = [this](brls::View *view) { + auto register_host_cb = [this, display_name, address, ps_version](brls::View *view) { bool err = false; - if(this->remote_display_name.length() <= 0) + std::string dn = display_name->getValue(); + std::string addr = address->getValue(); + ChiakiTarget version = CHIAKI_TARGET_PS4_UNKNOWN; + + switch(ps_version->getSelectedValue()) + { + case 0: + // ps5 v1 + version = CHIAKI_TARGET_PS5_1; + break; + case 1: + // ps4 v8 + version = CHIAKI_TARGET_PS4_10; + break; + case 2: + // ps4 v7 + version = CHIAKI_TARGET_PS4_9; + break; + case 3: + // ps4 v6 + version = CHIAKI_TARGET_PS4_8; + break; + } + + if(dn.length() <= 0) { brls::Application::notify("No Display name defined"); err = true; } - if(this->remote_addr.length() <= 0) + if(addr.length() <= 0) { brls::Application::notify("No Remote address provided"); err = true; } - if(this->remote_ps_version < 0) + if(version <= CHIAKI_TARGET_PS4_UNKNOWN) { brls::Application::notify("No PlayStation Version provided"); err = true; @@ -549,10 +527,9 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) if(err) return; - Host *host = this->settings->GetOrCreateHost(&this->remote_display_name); - host->SetHostAddr(this->remote_addr); - host->SetChiakiTarget(this->remote_ps_version); - + Host *host = this->settings->GetOrCreateHost(&dn); + host->SetHostAddr(addr); + host->SetChiakiTarget(version); HostInterface::Register(host); }; register_host->getClickEvent()->subscribe(register_host_cb); diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 6db9c50..50fdd2f 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -72,7 +72,7 @@ int Host::Wakeup() return ret; } -int Host::Register(std::string pin) +int Host::Register(int pin) { // use pin and accont_id to negociate secrets for session // @@ -103,7 +103,7 @@ int Host::Register(std::string pin) if(online_id.length() > 0) { regist_info.psn_online_id = this->psn_online_id.c_str(); - // regist_info.psn_account_id = {0}; + // regist_info.psn_account_id = '\0'; } else { @@ -117,16 +117,15 @@ int Host::Register(std::string pin) throw Exception("Undefined PS4 system version (please run discover first)"); } - this->regist_info.pin = atoi(pin.c_str()); this->regist_info.host = this->host_addr.c_str(); this->regist_info.broadcast = false; if(this->target >= CHIAKI_TARGET_PS4_9) - CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%s`", - this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin.c_str()); + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%d`", + this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin); else - CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN OnlineID `%s` pin `%s`", - this->host_name.c_str(), this->host_addr.c_str(), online_id.c_str(), pin.c_str()); + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN OnlineID `%s` pin `%d`", + this->host_name.c_str(), this->host_addr.c_str(), online_id.c_str(), pin); chiaki_regist_start(&this->regist, this->log, &this->regist_info, RegistEventCB, this); return HOST_REGISTER_OK; diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 3104bd4..e086215 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -345,42 +345,6 @@ bool IO::FreeVideo() return ret; } -bool IO::ReadUserKeyboard(char *buffer, size_t buffer_size) -{ -#ifndef __SWITCH__ - // use cin to get user input from linux - std::cin.getline(buffer, buffer_size); - CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); -#else - // https://kvadevack.se/post/nintendo-switch-virtual-keyboard/ - SwkbdConfig kbd; - Result rc = swkbdCreate(&kbd, 0); - - if(R_SUCCEEDED(rc)) - { - swkbdConfigMakePresetDefault(&kbd); - rc = swkbdShow(&kbd, buffer, buffer_size); - - if(R_SUCCEEDED(rc)) - { - CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); - } - else - { - CHIAKI_LOGE(this->log, "swkbdShow() error: %u\n", rc); - return false; - } - swkbdClose(&kbd); - } - else - { - CHIAKI_LOGE(this->log, "swkbdCreate() error: %u\n", rc); - return false; - } -#endif - return true; -} - bool IO::ReadGameTouchScreen(ChiakiControllerState *state) { #ifdef __SWITCH__ From c5246541a92cff966105cd08ec1218e3da287e7c Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 4 Jan 2021 23:04:04 +0100 Subject: [PATCH 017/104] Fix switch target chiaki.conf format --- switch/src/settings.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index f5edf1f..8cb5264 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -210,9 +210,8 @@ int Settings::WriteFile() config_file << "[" << it->first << "]\n" << "host_addr = \"" << it->second.GetHostAddr() << "\"\n" - << "target = " << it->second.GetChiakiTarget() << "\"\n"; + << "target = \"" << it->second.GetChiakiTarget() << "\"\n"; - config_file << "target = \"" << it->second.psn_account_id << "\"\n"; if(it->second.video_resolution) config_file << "video_resolution = \"" << this->ResolutionPresetToString(this->GetVideoResolution(&it->second)) From 3272f47dc67259292758ed69998e4c0b5c4ff717 Mon Sep 17 00:00:00 2001 From: Roy P Date: Wed, 6 Jan 2021 15:56:32 +0100 Subject: [PATCH 018/104] Update Flatpak to v2.0.1 --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 7cc6c43..3a03107 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://git.sr.ht/~thestr4ng3r/chiaki", - "tag": "v1.3.0", - "commit": "702d31eb01d37518e77f5c1be3ea493df9f18323" + "tag": "v2.0.1", + "commit": "9e698dd7c4e4011ff6e136741abef5cf4b32527c" } ] } From 0e324a41a0c2738cabfa487e111abc55c24185f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 5 Jan 2021 14:41:35 +0100 Subject: [PATCH 019/104] Fix setting a Feedback State Byte --- lib/src/feedback.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index d5a46bc..4c52b72 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -38,7 +38,7 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) { chiaki_feedback_state_format_v9(buf, state); - buf[0x10] = 0x0; + buf[0x19] = 0x0; buf[0x1a] = 0x0; buf[0x1b] = 0x1; // 1 for Shock, 0 for Sense } From abc9a27208592e34fce2fe850a35222f9f2efd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 11:31:13 +0100 Subject: [PATCH 020/104] Add Motion Stub to Setsu --- gui/src/streamsession.cpp | 2 +- setsu/CMakeLists.txt | 9 +- setsu/demo/{main.c => touchpad.c} | 6 +- setsu/include/setsu.h | 21 ++- setsu/src/setsu.c | 288 ++++++++++++++++++------------ 5 files changed, 196 insertions(+), 130 deletions(-) rename setsu/demo/{main.c => touchpad.c} (94%) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 01008a8..52915f0 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -409,7 +409,7 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: - setsu_connect(setsu, event->path); + setsu_connect(setsu, event->path, event->dev_type); break; case SETSU_EVENT_DEVICE_REMOVED: for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt index 90c1465..186f9c0 100644 --- a/setsu/CMakeLists.txt +++ b/setsu/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.2) project(libsetsu) -option(SETSU_BUILD_DEMO "Build testing executable for libsetsu" OFF) +option(SETSU_BUILD_DEMOS "Build testing executables for libsetsu" OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -17,9 +17,8 @@ find_package(Udev REQUIRED) find_package(Evdev REQUIRED) target_link_libraries(setsu Udev::libudev Evdev::libevdev) -if(SETSU_BUILD_DEMO) - add_executable(setsu-demo - demo/main.c) - target_link_libraries(setsu-demo setsu) +if(SETSU_BUILD_DEMOS) + add_executable(setsu-demo-touchpad demo/touchpad.c) + target_link_libraries(setsu-demo-touchpad setsu) endif() diff --git a/setsu/demo/main.c b/setsu/demo/touchpad.c similarity index 94% rename from setsu/demo/main.c rename to setsu/demo/touchpad.c index 4219638..5f7deff 100644 --- a/setsu/demo/main.c +++ b/setsu/demo/touchpad.c @@ -68,11 +68,15 @@ void event(SetsuEvent *event, void *user) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: { - SetsuDevice *dev = setsu_connect(setsu, event->path); + if(event->dev_type != SETSU_DEVICE_TYPE_TOUCHPAD) + break; + SetsuDevice *dev = setsu_connect(setsu, event->path, SETSU_DEVICE_TYPE_TOUCHPAD); LOG("Device added: %s, connect %s\n", event->path, dev ? "succeeded" : "FAILED!"); break; } case SETSU_EVENT_DEVICE_REMOVED: + if(event->dev_type != SETSU_DEVICE_TYPE_TOUCHPAD) + break; LOG("Device removed: %s\n", event->path); break; case SETSU_EVENT_TOUCH_DOWN: diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 5e546bc..a2ca4ed 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -13,13 +13,18 @@ typedef struct setsu_t Setsu; typedef struct setsu_device_t SetsuDevice; typedef int SetsuTrackingId; +typedef enum { + SETSU_DEVICE_TYPE_TOUCHPAD, + SETSU_DEVICE_TYPE_MOTION +} SetsuDeviceType; + typedef enum { /* New device available to connect. - * Event will have path set to the new device. */ + * Event will have path and type set to the new device. */ SETSU_EVENT_DEVICE_ADDED, /* Previously available device removed. - * Event will have path set to the new device. + * Event will have path and type set to the removed device. * Any SetsuDevice connected to this path will automatically * be disconnected and their pointers will be invalid immediately * after the callback for this event returns. */ @@ -53,7 +58,11 @@ typedef struct setsu_event_t SetsuEventType type; union { - const char *path; + struct + { + const char *path; + SetsuDeviceType dev_type; + }; struct { SetsuDevice *dev; @@ -75,11 +84,11 @@ typedef void (*SetsuEventCb)(SetsuEvent *event, void *user); Setsu *setsu_new(); void setsu_free(Setsu *setsu); void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user); -SetsuDevice *setsu_connect(Setsu *setsu, const char *path); +SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type); void setsu_disconnect(Setsu *setsu, SetsuDevice *dev); const char *setsu_device_get_path(SetsuDevice *dev); -uint32_t setsu_device_get_width(SetsuDevice *dev); -uint32_t setsu_device_get_height(SetsuDevice *dev); +uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev); +uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev); #ifdef __cplusplus } diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 819b174..b93ef2c 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -25,6 +25,7 @@ typedef struct setsu_avail_device_t { struct setsu_avail_device_t *next; + SetsuDeviceType type; char *path; bool connect_dirty; // whether the connect has not been sent as an event yet bool disconnect_dirty; // whether the disconnect has not been sent as an event yet @@ -36,27 +37,38 @@ typedef struct setsu_device_t { struct setsu_device_t *next; char *path; + SetsuDeviceType type; int fd; struct libevdev *evdev; - int min_x, min_y, max_x, max_y; - struct { - /* Saves the old tracking id that was just up-ed. - * also for handling "atomic" up->down - * i.e. when there is an up, then down with a different tracking id - * in a single frame (before SYN_REPORT), this saves the old - * tracking id that must be reported as up. */ - int tracking_id_prev; + union + { + struct + { + int min_x, min_y, max_x, max_y; - int tracking_id; - int x, y; - bool downed; - bool pos_dirty; - } slots[SLOTS_COUNT]; - unsigned int slot_cur; + struct + { + /* Saves the old tracking id that was just up-ed. + * also for handling "atomic" up->down + * i.e. when there is an up, then down with a different tracking id + * in a single frame (before SYN_REPORT), this saves the old + * tracking id that must be reported as up. */ + int tracking_id_prev; - uint64_t buttons_prev; - uint64_t buttons_cur; + int tracking_id; + int x, y; + bool downed; + bool pos_dirty; + } slots[SLOTS_COUNT]; + unsigned int slot_cur; + uint64_t buttons_prev; + uint64_t buttons_cur; + } touchpad; + struct + { + } motion; + }; } SetsuDevice; struct setsu_t @@ -131,8 +143,8 @@ static void scan_udev(Setsu *setsu) if(udev_enumerate_add_match_subsystem(udev_enum, "input") < 0) goto beach; - if(udev_enumerate_add_match_property(udev_enum, "ID_INPUT_TOUCHPAD", "1") < 0) - goto beach; + //if(udev_enumerate_add_match_property(udev_enum, "ID_INPUT_TOUCHPAD", "1") < 0) + // goto beach; if(udev_enumerate_scan_devices(udev_enum) < 0) goto beach; @@ -153,7 +165,7 @@ beach: udev_enumerate_unref(udev_enum); } -static bool is_device_interesting(struct udev_device *dev) +static bool is_device_interesting(struct udev_device *dev, SetsuDeviceType *type) { static const uint32_t device_ids[] = { // vendor id, model id @@ -162,8 +174,18 @@ static bool is_device_interesting(struct udev_device *dev) 0x54c, 0x0ce6 // DualSense }; - // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: - if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) + const char *touchpad_str = udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD"); + const char *accel_str = udev_device_get_property_value(dev, "ID_INPUT_ACCELEROMETER"); + if(touchpad_str && !strcmp(touchpad_str, "1")) + { + // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: + if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) + return false; + *type = SETSU_DEVICE_TYPE_TOUCHPAD; + } + else if(touchpad_str && !strcmp(touchpad_str, "1")) + *type = SETSU_DEVICE_TYPE_MOTION; + else return false; uint32_t vendor; @@ -221,12 +243,14 @@ static void update_udev_device(Setsu *setsu, struct udev_device *dev) } // not yet added - if(!is_device_interesting(dev)) + SetsuDeviceType type; + if(!is_device_interesting(dev, &type)) return; SetsuAvailDevice *adev = calloc(1, sizeof(SetsuAvailDevice)); if(!adev) return; + adev->type = type; adev->path = strdup(path); if(!adev->path) { @@ -272,7 +296,7 @@ bool get_dev_ids(const char *path, uint32_t *vendor_id, uint32_t *model_id) return true; } -SetsuDevice *setsu_connect(Setsu *setsu, const char *path) +SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type) { SetsuDevice *dev = calloc(1, sizeof(SetsuDevice)); if(!dev) @@ -281,6 +305,7 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path) dev->path = strdup(path); if(!dev->path) goto error; + dev->type = type; dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); if(dev->fd == -1) @@ -292,15 +317,23 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path) goto error; } - dev->min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); - dev->min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); - dev->max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); - dev->max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); - - for(size_t i=0; islots[i].tracking_id_prev = -1; - dev->slots[i].tracking_id = -1; + case SETSU_DEVICE_TYPE_TOUCHPAD: + dev->touchpad.min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); + dev->touchpad.min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); + dev->touchpad.max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); + dev->touchpad.max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); + + for(size_t i=0; itouchpad.slots[i].tracking_id_prev = -1; + dev->touchpad.slots[i].tracking_id = -1; + } + break; + case SETSU_DEVICE_TYPE_MOTION: + // TODO: init to defaults + break; } dev->next = setsu->dev; @@ -342,14 +375,18 @@ const char *setsu_device_get_path(SetsuDevice *dev) return dev->path; } -uint32_t setsu_device_get_width(SetsuDevice *dev) +uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev) { - return dev->max_x - dev->min_x; + if(dev->type != SETSU_DEVICE_TYPE_TOUCHPAD) + return 0; + return dev->touchpad.max_x - dev->touchpad.min_x; } -uint32_t setsu_device_get_height(SetsuDevice *dev) +uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev) { - return dev->max_y - dev->min_y; + if(dev->type != SETSU_DEVICE_TYPE_TOUCHPAD) + return 0; + return dev->touchpad.max_y - dev->touchpad.min_y; } void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) @@ -466,59 +503,68 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, libevdev_event_code_get_name(ev->type, ev->code), ev->value); #endif -#define S dev->slots[dev->slot_cur] - switch(ev->type) + if(ev->type == EV_SYN && ev->code == SYN_REPORT) { - case EV_ABS: - switch(ev->code) + device_drain(setsu, dev, cb, user); + return; + } + switch(dev->type) + { + case SETSU_DEVICE_TYPE_TOUCHPAD: + switch(ev->type) { - case ABS_MT_SLOT: - if((unsigned int)ev->value >= SLOTS_COUNT) + case EV_ABS: +#define S dev->touchpad.slots[dev->touchpad.slot_cur] + switch(ev->code) { - SETSU_LOG("slot too high\n"); + case ABS_MT_SLOT: + if((unsigned int)ev->value >= SLOTS_COUNT) + { + SETSU_LOG("slot too high\n"); + break; + } + dev->touchpad.slot_cur = ev->value; + break; + case ABS_MT_TRACKING_ID: + if(S.tracking_id != -1 && S.tracking_id_prev == -1) + { + // up the tracking id + S.tracking_id_prev = S.tracking_id; + // reset the rest + S.x = S.y = 0; + S.pos_dirty = false; + } + S.tracking_id = ev->value; + if(ev->value != -1) + S.downed = true; + break; + case ABS_MT_POSITION_X: + S.x = ev->value; + S.pos_dirty = true; + break; + case ABS_MT_POSITION_Y: + S.y = ev->value; + S.pos_dirty = true; + break; + } + break; +#undef S + case EV_KEY: { + uint64_t button = button_from_evdev(ev->code); + if(!button) break; - } - dev->slot_cur = ev->value; - break; - case ABS_MT_TRACKING_ID: - if(S.tracking_id != -1 && S.tracking_id_prev == -1) - { - // up the tracking id - S.tracking_id_prev = S.tracking_id; - // reset the rest - S.x = S.y = 0; - S.pos_dirty = false; - } - S.tracking_id = ev->value; - if(ev->value != -1) - S.downed = true; - break; - case ABS_MT_POSITION_X: - S.x = ev->value; - S.pos_dirty = true; - break; - case ABS_MT_POSITION_Y: - S.y = ev->value; - S.pos_dirty = true; + if(ev->value) + dev->touchpad.buttons_cur |= button; + else + dev->touchpad.buttons_cur &= ~button; break; + } } break; - case EV_KEY: { - uint64_t button = button_from_evdev(ev->code); - if(!button) - break; - if(ev->value) - dev->buttons_cur |= button; - else - dev->buttons_cur &= ~button; - break; - } - case EV_SYN: - if(ev->code == SYN_REPORT) - device_drain(setsu, dev, cb, user); + case SETSU_DEVICE_TYPE_MOTION: + // TODO: handle the events break; } -#undef S } static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user) @@ -526,48 +572,56 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * SetsuEvent event; #define BEGIN_EVENT(tp) do { memset(&event, 0, sizeof(event)); event.dev = dev; event.type = tp; } while(0) #define SEND_EVENT() do { cb(&event, user); } while (0) - for(size_t i=0; itype) { - if(dev->slots[i].tracking_id_prev != -1) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); - event.tracking_id = dev->slots[i].tracking_id_prev; - SEND_EVENT(); - dev->slots[i].tracking_id_prev = -1; - } - if(dev->slots[i].downed) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); - event.tracking_id = dev->slots[i].tracking_id; - SEND_EVENT(); - dev->slots[i].downed = false; - } - if(dev->slots[i].pos_dirty) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); - event.tracking_id = dev->slots[i].tracking_id; - event.x = (uint32_t)(dev->slots[i].x - dev->min_x); - event.y = (uint32_t)(dev->slots[i].y - dev->min_y); - SEND_EVENT(); - dev->slots[i].pos_dirty = false; - } - } + case SETSU_DEVICE_TYPE_TOUCHPAD: + for(size_t i=0; itouchpad.slots[i].tracking_id_prev != -1) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); + event.tracking_id = dev->touchpad.slots[i].tracking_id_prev; + SEND_EVENT(); + dev->touchpad.slots[i].tracking_id_prev = -1; + } + if(dev->touchpad.slots[i].downed) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); + event.tracking_id = dev->touchpad.slots[i].tracking_id; + SEND_EVENT(); + dev->touchpad.slots[i].downed = false; + } + if(dev->touchpad.slots[i].pos_dirty) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); + event.tracking_id = dev->touchpad.slots[i].tracking_id; + event.x = (uint32_t)(dev->touchpad.slots[i].x - dev->touchpad.min_x); + event.y = (uint32_t)(dev->touchpad.slots[i].y - dev->touchpad.min_y); + SEND_EVENT(); + dev->touchpad.slots[i].pos_dirty = false; + } + } - uint64_t buttons_diff = dev->buttons_prev ^ dev->buttons_cur; - for(uint64_t i=0; i<64; i++) - { - if(buttons_diff & 1) - { - uint64_t button = 1 << i; - BEGIN_EVENT((dev->buttons_cur & button) ? SETSU_EVENT_BUTTON_DOWN : SETSU_EVENT_BUTTON_UP); - event.button = button; - SEND_EVENT(); - } - buttons_diff >>= 1; - if(!buttons_diff) + uint64_t buttons_diff = dev->touchpad.buttons_prev ^ dev->touchpad.buttons_cur; + for(uint64_t i=0; i<64; i++) + { + if(buttons_diff & 1) + { + uint64_t button = 1 << i; + BEGIN_EVENT((dev->touchpad.buttons_cur & button) ? SETSU_EVENT_BUTTON_DOWN : SETSU_EVENT_BUTTON_UP); + event.button = button; + SEND_EVENT(); + } + buttons_diff >>= 1; + if(!buttons_diff) + break; + } + dev->touchpad.buttons_prev = dev->touchpad.buttons_cur; + break; + case SETSU_DEVICE_TYPE_MOTION: + // TODO break; } - dev->buttons_prev = dev->buttons_cur; #undef BEGIN_EVENT #undef SEND_EVENT } From 20c54b05ad9afc42ce083ba3e6556deb0f137f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 11:35:56 +0100 Subject: [PATCH 021/104] Print error on Setsu open failure --- setsu/src/setsu.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index b93ef2c..520de61 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -309,7 +309,11 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type) dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); if(dev->fd == -1) + { + SETSU_LOG("Failed to open %s\n", dev->path); + perror("setsu_connect"); goto error; + } if(libevdev_new_from_fd(dev->fd, &dev->evdev) < 0) { From 698bce80225bfc90c484ba7f790127fab17dc418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 13:04:24 +0100 Subject: [PATCH 022/104] Connect Motion Devices in Setsu --- setsu/CMakeLists.txt | 2 + setsu/demo/motion.c | 101 ++++++++++++++++++++++++++++++++++++++++++ setsu/demo/touchpad.c | 2 + setsu/src/setsu.c | 11 ++++- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 setsu/demo/motion.c diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt index 186f9c0..2dedb0c 100644 --- a/setsu/CMakeLists.txt +++ b/setsu/CMakeLists.txt @@ -20,5 +20,7 @@ target_link_libraries(setsu Udev::libudev Evdev::libevdev) if(SETSU_BUILD_DEMOS) add_executable(setsu-demo-touchpad demo/touchpad.c) target_link_libraries(setsu-demo-touchpad setsu) + add_executable(setsu-demo-motion demo/motion.c) + target_link_libraries(setsu-demo-motion setsu) endif() diff --git a/setsu/demo/motion.c b/setsu/demo/motion.c new file mode 100644 index 0000000..f66e691 --- /dev/null +++ b/setsu/demo/motion.c @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#include + +#include +#include +#include +#include +#include +#include + +Setsu *setsu; + +bool dirty = false; +bool log_mode; +volatile bool should_quit; + +#define LOG(...) do { if(log_mode) fprintf(stderr, __VA_ARGS__); } while(0) + +void sigint(int s) +{ + should_quit = true; +} + +void print_state() +{ +#if 0 + char buf[256]; + *buf = 0; + printf("\033[2J%s", buf); + fflush(stdout); +#endif +} + +void event(SetsuEvent *event, void *user) +{ + dirty = true; + switch(event->type) + { + case SETSU_EVENT_DEVICE_ADDED: { + if(event->dev_type != SETSU_DEVICE_TYPE_MOTION) + break; + SetsuDevice *dev = setsu_connect(setsu, event->path, SETSU_DEVICE_TYPE_MOTION); + LOG("Device added: %s, connect %s\n", event->path, dev ? "succeeded" : "FAILED!"); + break; + } + case SETSU_EVENT_DEVICE_REMOVED: + if(event->dev_type != SETSU_DEVICE_TYPE_MOTION) + break; + LOG("Device removed: %s\n", event->path); + break; + // TODO: motion events + default: + break; + } +} + +void usage(const char *prog) +{ + printf("usage: %s [-l]\n -l log mode\n", prog); + exit(1); +} + +int main(int argc, const char *argv[]) +{ + log_mode = false; + if(argc == 2) + { + if(!strcmp(argv[1], "-l")) + log_mode = true; + else + usage(argv[0]); + } + else if(argc != 1) + usage(argv[0]); + + setsu = setsu_new(); + if(!setsu) + { + printf("Failed to init setsu\n"); + return 1; + } + + struct sigaction sa = {0}; + sa.sa_handler = sigint; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + + dirty = true; + while(!should_quit) + { + if(dirty && !log_mode) + print_state(); + dirty = false; + setsu_poll(setsu, event, NULL); + } + setsu_free(setsu); + printf("\nさよなら!\n"); + return 0; +} + diff --git a/setsu/demo/touchpad.c b/setsu/demo/touchpad.c index 5f7deff..18757d4 100644 --- a/setsu/demo/touchpad.c +++ b/setsu/demo/touchpad.c @@ -122,6 +122,8 @@ void event(SetsuEvent *event, void *user) LOG("Button for %s: %llu %s\n", setsu_device_get_path(event->dev), (unsigned long long)event->button, event->type == SETSU_EVENT_BUTTON_DOWN ? "down" : "up"); break; + default: + break; } } diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 520de61..85bd41b 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -178,13 +178,18 @@ static bool is_device_interesting(struct udev_device *dev, SetsuDeviceType *type const char *accel_str = udev_device_get_property_value(dev, "ID_INPUT_ACCELEROMETER"); if(touchpad_str && !strcmp(touchpad_str, "1")) { - // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: + // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) return false; *type = SETSU_DEVICE_TYPE_TOUCHPAD; } - else if(touchpad_str && !strcmp(touchpad_str, "1")) + else if(accel_str && !strcmp(accel_str, "1")) + { + // Filter /dev/input/js* away and keep /dev/input/event* + if(!udev_device_get_property_value(dev, "ID_INPUT_WIDTH_MM")) + return false; *type = SETSU_DEVICE_TYPE_MOTION; + } else return false; @@ -435,6 +440,7 @@ void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) SetsuEvent event = { 0 }; event.type = SETSU_EVENT_DEVICE_ADDED; event.path = adev->path; + event.dev_type = adev->type; cb(&event, user); adev->connect_dirty = false; } @@ -443,6 +449,7 @@ void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) SetsuEvent event = { 0 }; event.type = SETSU_EVENT_DEVICE_REMOVED; event.path = adev->path; + event.dev_type = adev->type; cb(&event, user); // kill the device only after sending the event SetsuAvailDevice *next = adev->next; From 88c03aa7441ff20a1b81ce82d33de01f9ec8ef6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 14:23:26 +0100 Subject: [PATCH 023/104] Finish Motion in Setsu --- gui/src/streamsession.cpp | 8 ++-- setsu/demo/motion.c | 72 +++++++++++++++++++++++++++++++++--- setsu/demo/touchpad.c | 14 +++---- setsu/include/setsu.h | 14 ++++++- setsu/src/setsu.c | 78 +++++++++++++++++++++++++++++++++++---- 5 files changed, 160 insertions(+), 26 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 52915f0..97a1305 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -429,7 +429,7 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) case SETSU_EVENT_TOUCH_UP: for(auto it=setsu_ids.begin(); it!=setsu_ids.end(); it++) { - if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->tracking_id) + if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->touch.tracking_id) { chiaki_controller_state_stop_touch(&setsu_state, it.value()); setsu_ids.erase(it); @@ -439,18 +439,18 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) SendFeedbackState(); break; case SETSU_EVENT_TOUCH_POSITION: { - QPair k = { setsu_device_get_path(event->dev), event->tracking_id }; + QPair k = { setsu_device_get_path(event->dev), event->touch.tracking_id }; auto it = setsu_ids.find(k); if(it == setsu_ids.end()) { - int8_t cid = chiaki_controller_state_start_touch(&setsu_state, event->x, event->y); + int8_t cid = chiaki_controller_state_start_touch(&setsu_state, event->touch.x, event->touch.y); if(cid >= 0) setsu_ids[k] = (uint8_t)cid; else break; } else - chiaki_controller_state_set_touch_pos(&setsu_state, it.value(), event->x, event->y); + chiaki_controller_state_set_touch_pos(&setsu_state, it.value(), event->touch.x, event->touch.y); SendFeedbackState(); break; } diff --git a/setsu/demo/motion.c b/setsu/demo/motion.c index f66e691..0b0f88b 100644 --- a/setsu/demo/motion.c +++ b/setsu/demo/motion.c @@ -8,9 +8,29 @@ #include #include #include +#include Setsu *setsu; +#define NAME_LEN 8 +const char * const names[] = { + "accel x ", + "accel y ", + "accel z ", + " gyro x ", + " gyro y ", + " gyro z " +}; +union +{ + struct + { + float accel_x, accel_y, accel_z; + float gyro_x, gyro_y, gyro_z; + }; + float v[6]; +} vals; +uint32_t timestamp; bool dirty = false; bool log_mode; volatile bool should_quit; @@ -22,14 +42,44 @@ void sigint(int s) should_quit = true; } +#define BAR_LENGTH 100 +#define BAR_MAX 2.0f +#define BAR_MAX_GYRO 180.0f + void print_state() { -#if 0 - char buf[256]; - *buf = 0; + char buf[6 * (1 + NAME_LEN + BAR_LENGTH) + 1]; + size_t i = 0; + for(size_t b=0; b<6; b++) + { + buf[i++] = '\n'; + memcpy(buf + i, names[b], NAME_LEN); + i += NAME_LEN; + buf[i++] = '['; + size_t max = BAR_LENGTH-2; + for(size_t bi=0; bi 2 ? BAR_MAX_GYRO : BAR_MAX) * (2.0f * (float)(x) / (float)max - 1.0f)) + float cur = BAR_VAL(bi); + float prev = BAR_VAL((int)bi - 1); + if(prev < 0.0f && cur >= 0.0f) + { + buf[i++] = '|'; + continue; + } + bool cov = ((vals.v[b] < 0.0f) == (cur < 0.0f)) && fabsf(vals.v[b]) > fabsf(cur); + float next = BAR_VAL(bi + 1); +#define MARK_VAL (b > 2 ? 90.0f : 1.0f) + bool mark = cur < -MARK_VAL && next >= -MARK_VAL || prev < MARK_VAL && cur >= MARK_VAL; + buf[i++] = cov ? (mark ? '#' : '=') : (mark ? '.' : ' '); +#undef BAR_VAL + } + buf[i++] = ']'; + } + buf[i++] = '\0'; + assert(i == sizeof(buf)); printf("\033[2J%s", buf); fflush(stdout); -#endif } void event(SetsuEvent *event, void *user) @@ -49,7 +99,19 @@ void event(SetsuEvent *event, void *user) break; LOG("Device removed: %s\n", event->path); break; - // TODO: motion events + case SETSU_EVENT_MOTION: + LOG("Motion: %f, %f, %f / %f, %f, %f / %u\n", + event->motion.accel_x, event->motion.accel_y, event->motion.accel_z, + event->motion.gyro_x, event->motion.gyro_y, event->motion.gyro_z, + (unsigned int)event->motion.timestamp); + vals.accel_x = event->motion.accel_x; + vals.accel_y = event->motion.accel_y; + vals.accel_z = event->motion.accel_z; + vals.gyro_x = event->motion.gyro_x; + vals.gyro_y = event->motion.gyro_y; + vals.gyro_z = event->motion.gyro_z; + timestamp = event->motion.timestamp; + dirty = true; default: break; } diff --git a/setsu/demo/touchpad.c b/setsu/demo/touchpad.c index 18757d4..b51d628 100644 --- a/setsu/demo/touchpad.c +++ b/setsu/demo/touchpad.c @@ -80,13 +80,13 @@ void event(SetsuEvent *event, void *user) LOG("Device removed: %s\n", event->path); break; case SETSU_EVENT_TOUCH_DOWN: - LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); + LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->touch.tracking_id); for(size_t i=0; itracking_id; + touches[i].tracking_id = event->touch.tracking_id; break; } } @@ -94,19 +94,19 @@ void event(SetsuEvent *event, void *user) case SETSU_EVENT_TOUCH_POSITION: case SETSU_EVENT_TOUCH_UP: if(event->type == SETSU_EVENT_TOUCH_UP) - LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); + LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->touch.tracking_id); else LOG("Position for %s, tracking id %d: %u, %u\n", setsu_device_get_path(event->dev), - event->tracking_id, (unsigned int)event->x, (unsigned int)event->y); + event->touch.tracking_id, (unsigned int)event->touch.x, (unsigned int)event->touch.y); for(size_t i=0; itracking_id) + if(touches[i].down && touches[i].tracking_id == event->touch.tracking_id) { switch(event->type) { case SETSU_EVENT_TOUCH_POSITION: - touches[i].x = event->x; - touches[i].y = event->y; + touches[i].x = event->touch.x; + touches[i].y = event->touch.y; break; case SETSU_EVENT_TOUCH_UP: touches[i].down = false; diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index a2ca4ed..36ecf03 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -46,7 +46,10 @@ typedef enum { SETSU_EVENT_BUTTON_DOWN, /* Event will have dev and button set. */ - SETSU_EVENT_BUTTON_UP + SETSU_EVENT_BUTTON_UP, + + /* Event will have motion set. */ + SETSU_EVENT_MOTION } SetsuEventType; #define SETSU_BUTTON_0 (1u << 0) @@ -72,8 +75,14 @@ typedef struct setsu_event_t { SetsuTrackingId tracking_id; uint32_t x, y; - }; + } touch; SetsuButton button; + struct + { + float accel_x, accel_y, accel_z; // unit is 1G + float gyro_x, gyro_y, gyro_z; // unit is deg/sec + uint32_t timestamp; // microseconds + } motion; }; }; }; @@ -90,6 +99,7 @@ const char *setsu_device_get_path(SetsuDevice *dev); uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev); uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev); + #ifdef __cplusplus } #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 85bd41b..6395313 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -67,6 +67,12 @@ typedef struct setsu_device_t } touchpad; struct { + int accel_res_x, accel_res_y, accel_res_z; + int gyro_res_x, gyro_res_y, gyro_res_z; + int accel_x, accel_y, accel_z; + int gyro_x, gyro_y, gyro_z; + uint32_t timestamp; + bool dirty; } motion; }; } SetsuDevice; @@ -341,7 +347,13 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type) } break; case SETSU_DEVICE_TYPE_MOTION: - // TODO: init to defaults + dev->motion.accel_res_x = libevdev_get_abs_resolution(dev->evdev, ABS_X); + dev->motion.accel_res_y = libevdev_get_abs_resolution(dev->evdev, ABS_Y); + dev->motion.accel_res_z = libevdev_get_abs_resolution(dev->evdev, ABS_Z); + dev->motion.gyro_res_x = libevdev_get_abs_resolution(dev->evdev, ABS_RX); + dev->motion.gyro_res_y = libevdev_get_abs_resolution(dev->evdev, ABS_RY); + dev->motion.gyro_res_z = libevdev_get_abs_resolution(dev->evdev, ABS_RZ); + dev->motion.accel_y = dev->motion.accel_res_y; // 1G down break; } @@ -573,7 +585,45 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, } break; case SETSU_DEVICE_TYPE_MOTION: - // TODO: handle the events + switch(ev->type) + { + case EV_ABS: + switch(ev->code) + { + case ABS_X: + dev->motion.accel_x = ev->value; + dev->motion.dirty = true; + break; + case ABS_Y: + dev->motion.accel_y = ev->value; + dev->motion.dirty = true; + break; + case ABS_Z: + dev->motion.accel_z = ev->value; + dev->motion.dirty = true; + break; + case ABS_RX: + dev->motion.gyro_x = ev->value; + dev->motion.dirty = true; + break; + case ABS_RY: + dev->motion.gyro_y = ev->value; + dev->motion.dirty = true; + break; + case ABS_RZ: + dev->motion.gyro_z = ev->value; + dev->motion.dirty = true; + break; + } + break; + case EV_MSC: + if(ev->code == MSC_TIMESTAMP) + { + dev->motion.timestamp = ev->value; + dev->motion.dirty = true; + } + break; + } break; } } @@ -591,23 +641,23 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * if(dev->touchpad.slots[i].tracking_id_prev != -1) { BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); - event.tracking_id = dev->touchpad.slots[i].tracking_id_prev; + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id_prev; SEND_EVENT(); dev->touchpad.slots[i].tracking_id_prev = -1; } if(dev->touchpad.slots[i].downed) { BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); - event.tracking_id = dev->touchpad.slots[i].tracking_id; + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id; SEND_EVENT(); dev->touchpad.slots[i].downed = false; } if(dev->touchpad.slots[i].pos_dirty) { BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); - event.tracking_id = dev->touchpad.slots[i].tracking_id; - event.x = (uint32_t)(dev->touchpad.slots[i].x - dev->touchpad.min_x); - event.y = (uint32_t)(dev->touchpad.slots[i].y - dev->touchpad.min_y); + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id; + event.touch.x = (uint32_t)(dev->touchpad.slots[i].x - dev->touchpad.min_x); + event.touch.y = (uint32_t)(dev->touchpad.slots[i].y - dev->touchpad.min_y); SEND_EVENT(); dev->touchpad.slots[i].pos_dirty = false; } @@ -630,7 +680,19 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * dev->touchpad.buttons_prev = dev->touchpad.buttons_cur; break; case SETSU_DEVICE_TYPE_MOTION: - // TODO + if(dev->motion.dirty) + { + BEGIN_EVENT(SETSU_EVENT_MOTION); + event.motion.accel_x = (float)dev->motion.accel_x / (float)dev->motion.accel_res_x; + event.motion.accel_y = (float)dev->motion.accel_y / (float)dev->motion.accel_res_y; + event.motion.accel_z = (float)dev->motion.accel_z / (float)dev->motion.accel_res_z; + event.motion.gyro_x = (float)dev->motion.gyro_x / (float)dev->motion.gyro_res_x; + event.motion.gyro_y = (float)dev->motion.gyro_y / (float)dev->motion.gyro_res_y; + event.motion.gyro_z = (float)dev->motion.gyro_z / (float)dev->motion.gyro_res_z; + event.motion.timestamp = dev->motion.timestamp; + SEND_EVENT(); + dev->motion.dirty = false; + } break; } #undef BEGIN_EVENT From 32e1539c2201b1c46856919543b716f5be1affa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 15:47:42 +0100 Subject: [PATCH 024/104] Add Orientation Tracker --- lib/CMakeLists.txt | 6 +- lib/include/chiaki/orientation.h | 43 +++++++++++ lib/src/orientation.c | 126 +++++++++++++++++++++++++++++++ setsu/demo/motion.c | 4 +- setsu/include/setsu.h | 2 +- setsu/src/setsu.c | 9 ++- 6 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 lib/include/chiaki/orientation.h create mode 100644 lib/src/orientation.c diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index e2a79d3..36ac38c 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -35,7 +35,8 @@ set(HEADER_FILES include/chiaki/time.h include/chiaki/fec.h include/chiaki/regist.h - include/chiaki/opusdecoder.h) + include/chiaki/opusdecoder.h + include/chiaki/orientation.h) set(SOURCE_FILES src/common.c @@ -73,7 +74,8 @@ set(SOURCE_FILES src/time.c src/fec src/regist.c - src/opusdecoder.c) + src/opusdecoder.c + src/orientation.c) if(CHIAKI_ENABLE_FFMPEG_DECODER) list(APPEND HEADER_FILES include/chiaki/ffmpegdecoder.h) diff --git a/lib/include/chiaki/orientation.h b/lib/include/chiaki/orientation.h new file mode 100644 index 0000000..4a29c2e --- /dev/null +++ b/lib/include/chiaki/orientation.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#ifndef CHIAKI_ORIENTATION_H +#define CHIAKI_ORIENTATION_H + +#include "common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Quaternion orientation from accelerometer and gyroscope + * using Madgwick's algorithm. + * See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms + */ +typedef struct chiaki_orientation_t +{ + float x, y, z, w; +} ChiakiOrientation; + +CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient); +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); + +/** + * Extension of ChiakiOrientation, also tracking an absolute timestamp + */ +typedef struct chiaki_orientation_tracker_t +{ + ChiakiOrientation orient; + uint32_t timestamp; + bool first_sample; +} ChiakiOrientationTracker; + +CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker); +CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, + float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us); + +#ifdef __cplusplus +} +#endif + +#endif // CHIAKI_ORIENTATION_H diff --git a/lib/src/orientation.c b/lib/src/orientation.c new file mode 100644 index 0000000..90e991f --- /dev/null +++ b/lib/src/orientation.c @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#include + +CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient) +{ + orient->x = orient->y = orient->z = 0.0f; + orient->w = 1.0f; +} + +#define BETA 0.1f // 2 * proportional gain +static float inv_sqrt(float x); + +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) +{ + float q0 = orient->w, q1 = orient->x, q2 = orient->y, q3 = orient->z; + // Madgwick's IMU algorithm. + // See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms + float recip_norm; + float s0, s1, s2, s3; + float q_dot1, q_dot2, q_dot3, q_dot4; + float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2 ,_8q1, _8q2, q0q0, q1q1, q2q2, q3q3; + + // Rate of change of quaternion from gyroscope + q_dot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz); + q_dot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy); + q_dot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx); + q_dot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); + + // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation) + if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) + { + // Normalise accelerometer measurement + recip_norm = inv_sqrt(ax * ax + ay * ay + az * az); + ax *= recip_norm; + ay *= recip_norm; + az *= recip_norm; + + // Auxiliary variables to avoid repeated arithmetic + _2q0 = 2.0f * q0; + _2q1 = 2.0f * q1; + _2q2 = 2.0f * q2; + _2q3 = 2.0f * q3; + _4q0 = 4.0f * q0; + _4q1 = 4.0f * q1; + _4q2 = 4.0f * q2; + _8q1 = 8.0f * q1; + _8q2 = 8.0f * q2; + q0q0 = q0 * q0; + q1q1 = q1 * q1; + q2q2 = q2 * q2; + q3q3 = q3 * q3; + + // Gradient decent algorithm corrective step + s0 = _4q0 * q2q2 + _2q2 * ax + _4q0 * q1q1 - _2q1 * ay; + s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az; + s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az; + s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay; + recip_norm = inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude + s0 *= recip_norm; + s1 *= recip_norm; + s2 *= recip_norm; + s3 *= recip_norm; + + // Apply feedback step + q_dot1 -= BETA * s0; + q_dot2 -= BETA * s1; + q_dot3 -= BETA * s2; + q_dot4 -= BETA * s3; + } + + // Integrate rate of change of quaternion to yield quaternion + q0 += q_dot1 * time_step_sec; + q1 += q_dot2 * time_step_sec; + q2 += q_dot3 * time_step_sec; + q3 += q_dot4 * time_step_sec; + + // Normalise quaternion + recip_norm = inv_sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); + q0 *= recip_norm; + q1 *= recip_norm; + q2 *= recip_norm; + q3 *= recip_norm; + + orient->x = q1; + orient->y = q2; + orient->z = q3; + orient->w = q0; +} + +static float inv_sqrt(float x) +{ + // Fast inverse square-root + // See: http://en.wikipedia.org/wiki/Fast_inverse_square_root + float halfx = 0.5f * x; + float y = x; + long i = *(long*)&y; + i = 0x5f3759df - (i>>1); + y = *(float*)&i; + y = y * (1.5f - (halfx * y * y)); + return y; +} + +CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker) +{ + chiaki_orientation_init(&tracker->orient); + tracker->timestamp = 0; + tracker->first_sample = true; +} + +CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, + float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us) +{ + if(tracker->first_sample) + { + tracker->first_sample = false; + tracker->timestamp = timestamp_us; + return; + } + uint64_t delta_us = timestamp_us; + if(delta_us < tracker->timestamp) + delta_us += (1ULL << 32); + delta_us -= tracker->timestamp; + tracker->timestamp = timestamp_us; + chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, (float)delta_us / 1000000.0f); +} diff --git a/setsu/demo/motion.c b/setsu/demo/motion.c index 0b0f88b..ebb5fdd 100644 --- a/setsu/demo/motion.c +++ b/setsu/demo/motion.c @@ -44,7 +44,7 @@ void sigint(int s) #define BAR_LENGTH 100 #define BAR_MAX 2.0f -#define BAR_MAX_GYRO 180.0f +#define BAR_MAX_GYRO M_PI void print_state() { @@ -69,7 +69,7 @@ void print_state() } bool cov = ((vals.v[b] < 0.0f) == (cur < 0.0f)) && fabsf(vals.v[b]) > fabsf(cur); float next = BAR_VAL(bi + 1); -#define MARK_VAL (b > 2 ? 90.0f : 1.0f) +#define MARK_VAL (b > 2 ? 0.5f * M_PI : 1.0f) bool mark = cur < -MARK_VAL && next >= -MARK_VAL || prev < MARK_VAL && cur >= MARK_VAL; buf[i++] = cov ? (mark ? '#' : '=') : (mark ? '.' : ' '); #undef BAR_VAL diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 36ecf03..a3ed0e3 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -80,7 +80,7 @@ typedef struct setsu_event_t struct { float accel_x, accel_y, accel_z; // unit is 1G - float gyro_x, gyro_y, gyro_z; // unit is deg/sec + float gyro_x, gyro_y, gyro_z; // unit is rad/sec uint32_t timestamp; // microseconds } motion; }; diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 6395313..c31fe83 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -22,6 +23,8 @@ #define SETSU_LOG(...) do {} while(0) #endif +#define DEG2RAD (2.0f * M_PI / 360.0f) + typedef struct setsu_avail_device_t { struct setsu_avail_device_t *next; @@ -686,9 +689,9 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * event.motion.accel_x = (float)dev->motion.accel_x / (float)dev->motion.accel_res_x; event.motion.accel_y = (float)dev->motion.accel_y / (float)dev->motion.accel_res_y; event.motion.accel_z = (float)dev->motion.accel_z / (float)dev->motion.accel_res_z; - event.motion.gyro_x = (float)dev->motion.gyro_x / (float)dev->motion.gyro_res_x; - event.motion.gyro_y = (float)dev->motion.gyro_y / (float)dev->motion.gyro_res_y; - event.motion.gyro_z = (float)dev->motion.gyro_z / (float)dev->motion.gyro_res_z; + event.motion.gyro_x = DEG2RAD * (float)dev->motion.gyro_x / (float)dev->motion.gyro_res_x; + event.motion.gyro_y = DEG2RAD * (float)dev->motion.gyro_y / (float)dev->motion.gyro_res_y; + event.motion.gyro_z = DEG2RAD * (float)dev->motion.gyro_z / (float)dev->motion.gyro_res_z; event.motion.timestamp = dev->motion.timestamp; SEND_EVENT(); dev->motion.dirty = false; From 170dcd4d65c736e6e81a854f174f5d2fb34fe60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 16:35:01 +0100 Subject: [PATCH 025/104] Connect and read Setsu Motion Device in GUI --- gui/include/streamsession.h | 4 +++ gui/src/streamsession.cpp | 72 +++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 47dc217..cb1397e 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -13,6 +13,7 @@ #if CHIAKI_GUI_ENABLE_SETSU #include +#include #endif #include "exception.h" @@ -74,6 +75,9 @@ class StreamSession : public QObject Setsu *setsu; QMap, uint8_t> setsu_ids; ChiakiControllerState setsu_state; + SetsuDevice *setsu_motion_device; + ChiakiOrientationTracker orient_tracker; + bool orient_dirty; #endif ChiakiControllerState keyboard_state; diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 97a1305..dcec36d 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -153,11 +153,20 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje #endif #if CHIAKI_GUI_ENABLE_SETSU + setsu_motion_device = nullptr; chiaki_controller_state_set_idle(&setsu_state); + orient_dirty = false; + chiaki_orientation_tracker_init(&orient_tracker); setsu = setsu_new(); auto timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [this]{ setsu_poll(setsu, SessionSetsuCb, this); + if(orient_dirty) + { + // TODO: put orient/gyro/acc into setsu_state + // and SendFeedbackState(); + orient_dirty = false; + } }); timer->start(SETSU_UPDATE_INTERVAL_MS); #endif @@ -409,20 +418,56 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: - setsu_connect(setsu, event->path, event->dev_type); + switch(event->dev_type) + { + case SETSU_DEVICE_TYPE_TOUCHPAD: + // connect all the touchpads! + if(setsu_connect(setsu, event->path, event->dev_type)) + CHIAKI_LOGI(GetChiakiLog(), "Connected Setsu Touchpad Device %s", event->path); + else + CHIAKI_LOGE(GetChiakiLog(), "Failed to connect to Setsu Touchpad Device %s", event->path); + break; + case SETSU_DEVICE_TYPE_MOTION: + // connect only one motion since multiple make no sense + if(setsu_motion_device) + { + CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s detected there is already one connected", + event->path); + break; + } + setsu_motion_device = setsu_connect(setsu, event->path, event->dev_type); + if(setsu_motion_device) + CHIAKI_LOGI(GetChiakiLog(), "Connected Setsu Motion Device %s", event->path); + else + CHIAKI_LOGE(GetChiakiLog(), "Failed to connect to Setsu Motion Device %s", event->path); + break; + } break; case SETSU_EVENT_DEVICE_REMOVED: - for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) + switch(event->dev_type) { - if(it.key().first == event->path) - { - chiaki_controller_state_stop_touch(&setsu_state, it.value()); - setsu_ids.erase(it++); - } - else - it++; + case SETSU_DEVICE_TYPE_TOUCHPAD: + CHIAKI_LOGI(GetChiakiLog(), "Setsu Touchpad Device %s disconnected", event->path); + for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) + { + if(it.key().first == event->path) + { + chiaki_controller_state_stop_touch(&setsu_state, it.value()); + setsu_ids.erase(it++); + } + else + it++; + } + SendFeedbackState(); + break; + case SETSU_DEVICE_TYPE_MOTION: + if(!setsu_motion_device || strcmp(setsu_device_get_path(setsu_motion_device), event->path)) + break; + CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s disconnected", event->path); + setsu_motion_device = nullptr; + SendFeedbackState(); + break; } - SendFeedbackState(); break; case SETSU_EVENT_TOUCH_DOWN: break; @@ -460,6 +505,13 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) case SETSU_EVENT_BUTTON_UP: setsu_state.buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; break; + case SETSU_EVENT_MOTION: + chiaki_orientation_tracker_update(&orient_tracker, + event->motion.gyro_x, event->motion.gyro_y, event->motion.gyro_z, + event->motion.accel_x, event->motion.accel_y, event->motion.accel_z, + event->motion.timestamp); + orient_dirty = true; + break; } } #endif From cb827a525a1076b66371510e12e8605e48660bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 21:42:06 +0100 Subject: [PATCH 026/104] Add Motion to Feedback --- gui/src/streamsession.cpp | 18 ++++---- lib/include/chiaki/controller.h | 29 ++++--------- lib/include/chiaki/feedback.h | 3 ++ lib/include/chiaki/orientation.h | 10 ++++- lib/src/controller.c | 43 +++++++++++++++++++ lib/src/feedback.c | 73 ++++++++++++++++++++++++-------- lib/src/feedbacksender.c | 28 +++++++++++- lib/src/orientation.c | 28 +++++++++++- 8 files changed, 182 insertions(+), 50 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index dcec36d..7d25136 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -155,7 +155,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje #if CHIAKI_GUI_ENABLE_SETSU setsu_motion_device = nullptr; chiaki_controller_state_set_idle(&setsu_state); - orient_dirty = false; + orient_dirty = true; chiaki_orientation_tracker_init(&orient_tracker); setsu = setsu_new(); auto timer = new QTimer(this); @@ -163,8 +163,8 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje setsu_poll(setsu, SessionSetsuCb, this); if(orient_dirty) { - // TODO: put orient/gyro/acc into setsu_state - // and SendFeedbackState(); + chiaki_orientation_tracker_apply_to_controller_state(&orient_tracker, &setsu_state); + SendFeedbackState(); orient_dirty = false; } }); @@ -330,16 +330,17 @@ void StreamSession::SendFeedbackState() ChiakiControllerState state; chiaki_controller_state_set_idle(&state); +#if CHIAKI_GUI_ENABLE_SETSU + // setsu is the one that potentially has gyro/accel/orient so copy that directly first + state = setsu_state; +#endif + for(auto controller : controllers) { auto controller_state = controller->GetState(); chiaki_controller_state_or(&state, &state, &controller_state); } -#if CHIAKI_GUI_ENABLE_SETSU - chiaki_controller_state_or(&state, &state, &setsu_state); -#endif - chiaki_controller_state_or(&state, &state, &keyboard_state); chiaki_session_set_controller_state(&session, &state); } @@ -465,7 +466,8 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) break; CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s disconnected", event->path); setsu_motion_device = nullptr; - SendFeedbackState(); + chiaki_orientation_tracker_init(&orient_tracker); + orient_dirty = true; break; } break; diff --git a/lib/include/chiaki/controller.h b/lib/include/chiaki/controller.h index e8f2e7c..24e86a9 100644 --- a/lib/include/chiaki/controller.h +++ b/lib/include/chiaki/controller.h @@ -66,6 +66,10 @@ typedef struct chiaki_controller_state_t uint8_t touch_id_next; ChiakiControllerTouch touches[CHIAKI_CONTROLLER_TOUCHES_MAX]; + + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; + float orient_x, orient_y, orient_z, orient_w; } ChiakiControllerState; CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state); @@ -79,27 +83,12 @@ CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *sta CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y); -static inline bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) -{ - if(!(a->buttons == b->buttons - && a->l2_state == b->l2_state - && a->r2_state == b->r2_state - && a->left_x == b->left_x - && a->left_y == b->left_y - && a->right_x == b->right_x - && a->right_y == b->right_y)) - return false; - - for(size_t i=0; itouches[i].id != b->touches[i].id) - return false; - if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) - return false; - } - return true; -} +CHIAKI_EXPORT bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b); +/** + * Union of two controller states. + * Ignores gyro, accel and orient because it makes no sense there. + */ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, ChiakiControllerState *a, ChiakiControllerState *b); #ifdef __cplusplus diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 264af5c..be66384 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -15,6 +15,9 @@ extern "C" { typedef struct chiaki_feedback_state_t { + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; + float orient_x, orient_y, orient_z, orient_w; int16_t left_x; int16_t left_y; int16_t right_x; diff --git a/lib/include/chiaki/orientation.h b/lib/include/chiaki/orientation.h index 4a29c2e..2322d55 100644 --- a/lib/include/chiaki/orientation.h +++ b/lib/include/chiaki/orientation.h @@ -4,6 +4,7 @@ #define CHIAKI_ORIENTATION_H #include "common.h" +#include "controller.h" #ifdef __cplusplus extern "C" { @@ -20,13 +21,16 @@ typedef struct chiaki_orientation_t } ChiakiOrientation; CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient); -CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, + float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); /** - * Extension of ChiakiOrientation, also tracking an absolute timestamp + * Extension of ChiakiOrientation, also tracking an absolute timestamp and the current gyro/accel state */ typedef struct chiaki_orientation_tracker_t { + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; ChiakiOrientation orient; uint32_t timestamp; bool first_sample; @@ -35,6 +39,8 @@ typedef struct chiaki_orientation_tracker_t CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker); CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us); +CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, + ChiakiControllerState *state); #ifdef __cplusplus } diff --git a/lib/src/controller.c b/lib/src/controller.c index 0e3a522..21b9971 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -20,6 +20,14 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state state->touches[i].x = 0; state->touches[i].y = 0; } + state->gyro_x = state->gyro_y = state->gyro_z = 0.0f; + state->accel_x = 0.0f; + state->accel_y = 1.0f; + state->accel_z = 0.0f; + state->orient_x = 0.0f; + state->orient_y = 0.0f; + state->orient_z = 0.0f; + state->orient_w = 1.0f; } CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y) @@ -64,6 +72,41 @@ CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState * } } +CHIAKI_EXPORT bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) +{ + if(!(a->buttons == b->buttons + && a->l2_state == b->l2_state + && a->r2_state == b->r2_state + && a->left_x == b->left_x + && a->left_y == b->left_y + && a->right_x == b->right_x + && a->right_y == b->right_y)) + return false; + + for(size_t i=0; itouches[i].id != b->touches[i].id) + return false; + if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) + return false; + } + +#define CHECKF(n) if(a->n < b->n - 0.0000001f || a->n > b->n + 0.0000001f) return false + CHECKF(gyro_x); + CHECKF(gyro_y); + CHECKF(gyro_z); + CHECKF(accel_x); + CHECKF(accel_y); + CHECKF(accel_z); + CHECKF(orient_x); + CHECKF(orient_y); + CHECKF(orient_z); + CHECKF(orient_w); +#undef CHECKF + + return true; +} + #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define ABS(a) ((a) > 0 ? (a) : -(a)) #define MAX_ABS(a, b) (ABS(a) > ABS(b) ? (a) : (b)) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index 4c52b72..2ce0b98 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -9,26 +9,65 @@ #include #endif #include +#include + +#define GYRO_MIN -30.0f +#define GYRO_MAX 30.0f +#define ACCEL_MIN -5.0f +#define ACCEL_MAX 5.0f + +static uint32_t compress_quat(float *q) +{ + // very similar idea as https://github.com/jpreiss/quatcompress + size_t largest_i = 0; + for(size_t i = 1; i < 4; i++) + { + if(fabs(q[i]) > fabs(q[largest_i])) + largest_i = i; + } + uint32_t r = (q[largest_i] < 0.0 ? 1 : 0) | (largest_i << 1); + for(size_t i = 0; i < 3; i++) + { + size_t qi = i < largest_i ? i : i + 1; + float v = q[qi]; + if(v < -M_SQRT1_2) + v = -M_SQRT1_2; + if(v > M_SQRT1_2) + v = M_SQRT1_2; + v += M_SQRT1_2; + v *= (float)0x1ff / (2.0f * M_SQRT1_2); + r |= (uint32_t)v << (3 + i * 9); + } + return r; +} CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackState *state) { - buf[0x0] = 0xa0; // TODO - buf[0x1] = 0xff; // TODO - buf[0x2] = 0x7f; // TODO - buf[0x3] = 0xff; // TODO - buf[0x4] = 0x7f; // TODO - buf[0x5] = 0xff; // TODO - buf[0x6] = 0x7f; // TODO - buf[0x7] = 0xff; // TODO - buf[0x8] = 0x7f; // TODO - buf[0x9] = 0x99; // TODO - buf[0xa] = 0x99; // TODO - buf[0xb] = 0xff; // TODO - buf[0xc] = 0x7f; // TODO - buf[0xd] = 0xfe; // TODO - buf[0xe] = 0xf7; // TODO - buf[0xf] = 0xef; // TODO - buf[0x10] = 0x1f; // TODO + buf[0x0] = 0xa0; + uint16_t v = (uint16_t)(0xffff * ((float)state->gyro_x - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x1] = v; + buf[0x2] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->gyro_y - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x3] = v; + buf[0x4] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->gyro_z - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x5] = v; + buf[0x6] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_x - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0x7] = v; + buf[0x8] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_y - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0x9] = v; + buf[0xa] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_z - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0xb] = v; + buf[0xc] = v >> 8; + float q[4] = { state->orient_x, state->orient_y, state->orient_z, state->orient_w }; + uint32_t qc = compress_quat(q); + buf[0xd] = qc; + buf[0xe] = qc >> 0x8; + buf[0xf] = qc >> 0x10; + buf[0x10] = qc >> 0x18; *((chiaki_unaligned_uint16_t *)(buf + 0x11)) = htons((uint16_t)state->left_x); *((chiaki_unaligned_uint16_t *)(buf + 0x13)) = htons((uint16_t)state->left_y); *((chiaki_unaligned_uint16_t *)(buf + 0x15)) = htons((uint16_t)state->right_x); diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index dc67bf6..771796f 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -83,10 +83,24 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_sender_set_controller_state(Chiaki static bool controller_state_equals_for_feedback_state(ChiakiControllerState *a, ChiakiControllerState *b) { - return a->left_x == b->left_x + if(!(a->left_x == b->left_x && a->left_y == b->left_y && a->right_x == b->right_x - && a->right_y == b->right_y; + && a->right_y == b->right_y)) + return false; +#define CHECKF(n) if(a->n < b->n - 0.0000001f || a->n > b->n + 0.0000001f) return false + CHECKF(gyro_x); + CHECKF(gyro_y); + CHECKF(gyro_z); + CHECKF(accel_x); + CHECKF(accel_y); + CHECKF(accel_z); + CHECKF(orient_x); + CHECKF(orient_y); + CHECKF(orient_z); + CHECKF(orient_w); +#undef CHECKF + return true; } static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) @@ -96,6 +110,16 @@ static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) state.left_y = feedback_sender->controller_state.left_y; state.right_x = feedback_sender->controller_state.right_x; state.right_y = feedback_sender->controller_state.right_y; + state.gyro_x = feedback_sender->controller_state.gyro_x; + state.gyro_y = feedback_sender->controller_state.gyro_y; + state.gyro_z = feedback_sender->controller_state.gyro_z; + state.accel_x = feedback_sender->controller_state.accel_x; + state.accel_y = feedback_sender->controller_state.accel_y; + state.accel_z = feedback_sender->controller_state.accel_z; + state.orient_x = feedback_sender->controller_state.orient_x; + state.orient_y = feedback_sender->controller_state.orient_y; + state.orient_z = feedback_sender->controller_state.orient_z; + state.orient_w = feedback_sender->controller_state.orient_w; ChiakiErrorCode err = chiaki_takion_send_feedback_state(feedback_sender->takion, feedback_sender->state_seq_num++, &state); if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(feedback_sender->log, "FeedbackSender failed to send Feedback State"); diff --git a/lib/src/orientation.c b/lib/src/orientation.c index 90e991f..bb0849e 100644 --- a/lib/src/orientation.c +++ b/lib/src/orientation.c @@ -11,7 +11,8 @@ CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient) #define BETA 0.1f // 2 * proportional gain static float inv_sqrt(float x); -CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, + float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) { float q0 = orient->w, q1 = orient->x, q2 = orient->y, q3 = orient->z; // Madgwick's IMU algorithm. @@ -103,6 +104,10 @@ static float inv_sqrt(float x) CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker) { + tracker->accel_x = 0.0f; + tracker->accel_y = 1.0f; + tracker->accel_z = 0.0f; + tracker->gyro_x = tracker->gyro_y = tracker->gyro_z = 0.0f; chiaki_orientation_init(&tracker->orient); tracker->timestamp = 0; tracker->first_sample = true; @@ -111,6 +116,12 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tra CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us) { + tracker->gyro_x = gx; + tracker->gyro_y = gy; + tracker->gyro_z = gz; + tracker->accel_x = ax; + tracker->accel_y = ay; + tracker->accel_z = az; if(tracker->first_sample) { tracker->first_sample = false; @@ -124,3 +135,18 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *t tracker->timestamp = timestamp_us; chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, (float)delta_us / 1000000.0f); } + +CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, + ChiakiControllerState *state) +{ + state->gyro_x = tracker->gyro_x; + state->gyro_y = tracker->gyro_y; + state->gyro_z = tracker->gyro_z; + state->accel_x = tracker->accel_x; + state->accel_y = tracker->accel_y; + state->accel_z = tracker->accel_z; + state->orient_x = tracker->orient.x; + state->orient_y = tracker->orient.y; + state->orient_z = tracker->orient.z; + state->orient_w = tracker->orient.w; +} From 24d73064db228296468342eecd96f3f19140eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 21:51:18 +0100 Subject: [PATCH 027/104] Format Fixes --- lib/src/rpcrypt.c | 2 +- lib/src/session.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 30225c0..15b64d0 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -1698,7 +1698,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_aeropause(ChiakiTarget target, size CHIAKI_EXPORT void chiaki_rpcrypt_init_auth(ChiakiRPCrypt *rpcrypt, ChiakiTarget target, const uint8_t *nonce, const uint8_t *morning) { rpcrypt->target = target; - chiaki_rpcrypt_bright_ambassador(target, rpcrypt->bright, rpcrypt->ambassador, nonce, morning); + chiaki_rpcrypt_bright_ambassador(target, rpcrypt->bright, rpcrypt->ambassador, nonce, morning); } CHIAKI_EXPORT void chiaki_rpcrypt_init_regist_ps4_pre10(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, uint32_t pin) diff --git a/lib/src/session.c b/lib/src/session.c index 32cb646..b5b928f 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -609,7 +609,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch #ifdef _WIN32 CHIAKI_LOGE(session->log, "Failed to create socket to request session"); #else - CHIAKI_LOGE(session->log, "Failed to create socket to request session: %s", strerror(errno)); + CHIAKI_LOGE(session->log, "Failed to create socket to request session: %s", strerror(errno)); #endif free(sa); continue; From 42a3b864d07f7816085af5a79cf6f32c210abf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 22:04:53 +0100 Subject: [PATCH 028/104] Add _USE_MATH_DEFINES for Windows --- lib/src/feedback.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index 2ce0b98..1ae190e 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -1,5 +1,7 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL +#define _USE_MATH_DEFINES + #include #include From 96cbd5d9b8d4ead2592ccd897cb99e3d3d89d0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 9 Jan 2021 10:42:40 +0100 Subject: [PATCH 029/104] Fix Madgwick Filter for Orientation --- lib/include/chiaki/orientation.h | 4 +- lib/src/feedbacksender.c | 2 + lib/src/orientation.c | 68 +++++++++++++++++++++----------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/lib/include/chiaki/orientation.h b/lib/include/chiaki/orientation.h index 2322d55..6dde62b 100644 --- a/lib/include/chiaki/orientation.h +++ b/lib/include/chiaki/orientation.h @@ -22,7 +22,7 @@ typedef struct chiaki_orientation_t CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient); CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, - float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); + float gx, float gy, float gz, float ax, float ay, float az, float beta, float time_step_sec); /** * Extension of ChiakiOrientation, also tracking an absolute timestamp and the current gyro/accel state @@ -33,7 +33,7 @@ typedef struct chiaki_orientation_tracker_t float accel_x, accel_y, accel_z; ChiakiOrientation orient; uint32_t timestamp; - bool first_sample; + uint64_t sample_index; } ChiakiOrientationTracker; CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker); diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index 771796f..cafffff 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -116,10 +116,12 @@ static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) state.accel_x = feedback_sender->controller_state.accel_x; state.accel_y = feedback_sender->controller_state.accel_y; state.accel_z = feedback_sender->controller_state.accel_z; + state.orient_x = feedback_sender->controller_state.orient_x; state.orient_y = feedback_sender->controller_state.orient_y; state.orient_z = feedback_sender->controller_state.orient_z; state.orient_w = feedback_sender->controller_state.orient_w; + ChiakiErrorCode err = chiaki_takion_send_feedback_state(feedback_sender->takion, feedback_sender->state_seq_num++, &state); if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(feedback_sender->log, "FeedbackSender failed to send Feedback State"); diff --git a/lib/src/orientation.c b/lib/src/orientation.c index bb0849e..7f07b09 100644 --- a/lib/src/orientation.c +++ b/lib/src/orientation.c @@ -1,18 +1,30 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include +#include + +#define SIN_1_4_PI 0.7071067811865475 +#define SIN_NEG_1_4_PI -0.7071067811865475 +#define COS_1_4_PI 0.7071067811865476 +#define COS_NEG_1_4_PI 0.7071067811865476 + +#define WARMUP_SAMPLES_COUNT 30 +#define BETA_WARMUP 20.0f +#define BETA_DEFAULT 0.05f CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient) { - orient->x = orient->y = orient->z = 0.0f; - orient->w = 1.0f; + // 90 deg rotation around x for Madgwick + orient->x = SIN_1_4_PI; + orient->y = 0.0f; + orient->z = 0.0f; + orient->w = COS_1_4_PI; } -#define BETA 0.1f // 2 * proportional gain static float inv_sqrt(float x); CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, - float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) + float gx, float gy, float gz, float ax, float ay, float az, float beta, float time_step_sec) { float q0 = orient->w, q1 = orient->x, q2 = orient->y, q3 = orient->z; // Madgwick's IMU algorithm. @@ -57,17 +69,22 @@ CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az; s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az; s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay; - recip_norm = inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude - s0 *= recip_norm; - s1 *= recip_norm; - s2 *= recip_norm; - s3 *= recip_norm; + recip_norm = s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3; // normalise step magnitude + // avoid NaN when the orientation is already perfect or inverse to perfect + if(recip_norm > 0.000001f) + { + recip_norm = inv_sqrt(recip_norm); + s0 *= recip_norm; + s1 *= recip_norm; + s2 *= recip_norm; + s3 *= recip_norm; - // Apply feedback step - q_dot1 -= BETA * s0; - q_dot2 -= BETA * s1; - q_dot3 -= BETA * s2; - q_dot4 -= BETA * s3; + // Apply feedback step + q_dot1 -= beta * s0; + q_dot2 -= beta * s1; + q_dot3 -= beta * s2; + q_dot4 -= beta * s3; + } } // Integrate rate of change of quaternion to yield quaternion @@ -91,6 +108,9 @@ CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, static float inv_sqrt(float x) { +#if 1 + return 1.0f / sqrt(x); +#else // Fast inverse square-root // See: http://en.wikipedia.org/wiki/Fast_inverse_square_root float halfx = 0.5f * x; @@ -100,6 +120,7 @@ static float inv_sqrt(float x) y = *(float*)&i; y = y * (1.5f - (halfx * y * y)); return y; +#endif } CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker) @@ -110,7 +131,7 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tra tracker->gyro_x = tracker->gyro_y = tracker->gyro_z = 0.0f; chiaki_orientation_init(&tracker->orient); tracker->timestamp = 0; - tracker->first_sample = true; + tracker->sample_index = 0; } CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, @@ -122,9 +143,9 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *t tracker->accel_x = ax; tracker->accel_y = ay; tracker->accel_z = az; - if(tracker->first_sample) + tracker->sample_index++; + if(tracker->sample_index <= 1) { - tracker->first_sample = false; tracker->timestamp = timestamp_us; return; } @@ -133,7 +154,9 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *t delta_us += (1ULL << 32); delta_us -= tracker->timestamp; tracker->timestamp = timestamp_us; - chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, (float)delta_us / 1000000.0f); + chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, + tracker->sample_index < WARMUP_SAMPLES_COUNT ? BETA_WARMUP : BETA_DEFAULT, + (float)delta_us / 1000000.0f); } CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, @@ -145,8 +168,9 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOr state->accel_x = tracker->accel_x; state->accel_y = tracker->accel_y; state->accel_z = tracker->accel_z; - state->orient_x = tracker->orient.x; - state->orient_y = tracker->orient.y; - state->orient_z = tracker->orient.z; - state->orient_w = tracker->orient.w; + // -90 deg rotation around x from Madgwick + state->orient_w = COS_NEG_1_4_PI * tracker->orient.w - SIN_NEG_1_4_PI * tracker->orient.x; + state->orient_x = COS_NEG_1_4_PI * tracker->orient.x + SIN_NEG_1_4_PI * tracker->orient.w; + state->orient_y = COS_NEG_1_4_PI * tracker->orient.y - SIN_NEG_1_4_PI * tracker->orient.z; + state->orient_z = COS_NEG_1_4_PI * tracker->orient.z + SIN_NEG_1_4_PI * tracker->orient.y; } From 7785c310a9bb4e37c56263504a4b5df99685e49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 10 Jan 2021 11:04:19 +0100 Subject: [PATCH 030/104] Fix some Uninitialized Memory --- gui/src/streamsession.cpp | 3 ++- lib/src/frameprocessor.c | 2 ++ lib/src/senkusha.c | 2 +- lib/src/videoreceiver.c | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 7d25136..3f46070 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -102,11 +102,12 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje QByteArray host_str = connect_info.host.toUtf8(); - ChiakiConnectInfo chiaki_connect_info; + ChiakiConnectInfo chiaki_connect_info = {}; chiaki_connect_info.ps5 = chiaki_target_is_ps5(connect_info.target); chiaki_connect_info.host = host_str.constData(); chiaki_connect_info.video_profile = connect_info.video_profile; chiaki_connect_info.video_profile_auto_downgrade = true; + chiaki_connect_info.enable_keyboard = false; #if CHIAKI_LIB_ENABLE_PI_DECODER if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264) diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index c072a98..bcee36b 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -48,6 +48,8 @@ CHIAKI_EXPORT void chiaki_frame_processor_init(ChiakiFrameProcessor *frame_proce frame_processor->buf_stride_per_unit = 0; frame_processor->units_source_expected = 0; frame_processor->units_fec_expected = 0; + frame_processor->units_source_received = 0; + frame_processor->units_fec_received = 0; frame_processor->unit_slots = NULL; frame_processor->unit_slots_size = 0; frame_processor->flushed = true; diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index ebab5d7..fc4e600 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -361,7 +361,7 @@ static ChiakiErrorCode senkusha_run_mtu_in_test(ChiakiSenkusha *senkusha, uint32 senkusha->state_failed = false; senkusha->mtu_id = ++request_id; - tkproto_SenkushaMtuCommand mtu_cmd; + tkproto_SenkushaMtuCommand mtu_cmd = { 0 }; mtu_cmd.id = request_id; mtu_cmd.mtu_req = cur; mtu_cmd.num = 1; diff --git a/lib/src/videoreceiver.c b/lib/src/videoreceiver.c index 52beb02..6468fcd 100644 --- a/lib/src/videoreceiver.c +++ b/lib/src/videoreceiver.c @@ -17,6 +17,7 @@ CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receive video_receiver->frame_index_cur = -1; video_receiver->frame_index_prev = -1; + video_receiver->frame_index_prev_complete = 0; chiaki_frame_processor_init(&video_receiver->frame_processor, video_receiver->log); video_receiver->packet_stats = packet_stats; From a0c3768edb6ca6885b7a6d7b3cb8aa21469dd1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 10 Jan 2021 14:46:24 +0100 Subject: [PATCH 031/104] Fix Idle Controller State on Android --- android/app/src/main/cpp/chiaki-jni.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index 35543ad..49229f5 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -373,7 +373,8 @@ JNIEXPORT void JNICALL JNI_FCN(sessionSetSurface)(JNIEnv *env, jobject obj, jlon JNIEXPORT void JNICALL JNI_FCN(sessionSetControllerState)(JNIEnv *env, jobject obj, jlong ptr, jobject controller_state_java) { AndroidChiakiSession *session = (AndroidChiakiSession *)ptr; - ChiakiControllerState controller_state = { 0 }; + ChiakiControllerState controller_state; + chiaki_controller_state_set_idle(&controller_state); controller_state.buttons = (uint32_t)E->GetIntField(env, controller_state_java, session->java_controller_state_buttons); controller_state.l2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_l2_state); controller_state.r2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_r2_state); From 9ab84e60546d94be9ad726fb291fc92d9702c82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 10 Jan 2021 16:01:37 +0100 Subject: [PATCH 032/104] Prefer fixed local Port for Discovery --- lib/include/chiaki/discovery.h | 2 ++ lib/src/discovery.c | 49 ++++++++++++++++++++++++---------- lib/src/discoveryservice.c | 4 +-- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index c9881b5..f26200b 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -25,6 +25,8 @@ extern "C" { #define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4 "00020020" #define CHIAKI_DISCOVERY_PORT_PS5 9302 #define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5 "00030010" +#define CHIAKI_DISCOVERY_PORT_LOCAL_MIN 9303 +#define CHIAKI_DISCOVERY_PORT_LOCAL_MAX 9319 typedef enum chiaki_discovery_cmd_t { diff --git a/lib/src/discovery.c b/lib/src/discovery.c index 0658c87..f207a54 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -152,27 +152,48 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_init(ChiakiDiscovery *discovery, return CHIAKI_ERR_NETWORK; } - memset(&discovery->local_addr, 0, sizeof(discovery->local_addr)); - discovery->local_addr.sa_family = family; - if(family == AF_INET6) + // First try CHIAKI_DISCOVERY_PORT_LOCAL_MIN..local_addr, 0, sizeof(discovery->local_addr)); + discovery->local_addr.sa_family = family; + if(family == AF_INET6) + { #ifndef __SWITCH__ - struct in6_addr anyaddr = IN6ADDR_ANY_INIT; + struct in6_addr anyaddr = IN6ADDR_ANY_INIT; #endif - struct sockaddr_in6 *addr = (struct sockaddr_in6 *)&discovery->local_addr; + struct sockaddr_in6 *addr = (struct sockaddr_in6 *)&discovery->local_addr; #ifndef __SWITCH__ - addr->sin6_addr = anyaddr; + addr->sin6_addr = anyaddr; #endif - addr->sin6_port = htons(0); - } - else // AF_INET - { - struct sockaddr_in *addr = (struct sockaddr_in *)&discovery->local_addr; - addr->sin_addr.s_addr = htonl(INADDR_ANY); - addr->sin_port = htons(0); + addr->sin6_port = htons(port); + } + else // AF_INET + { + struct sockaddr_in *addr = (struct sockaddr_in *)&discovery->local_addr; + addr->sin_addr.s_addr = htonl(INADDR_ANY); + addr->sin_port = htons(port); + } + + r = bind(discovery->socket, &discovery->local_addr, sizeof(discovery->local_addr)); + if(r >= 0 || !port) + break; + if(port == CHIAKI_DISCOVERY_PORT_LOCAL_MAX) + { + port = 0; + CHIAKI_LOGI(discovery->log, "Discovery failed to bind port %u, trying random", + (unsigned int)port); + } + else + { + port++; + CHIAKI_LOGI(discovery->log, "Discovery failed to bind port %u, trying one higher", + (unsigned int)port); + } } - int r = bind(discovery->socket, &discovery->local_addr, sizeof(discovery->local_addr)); if(r < 0) { CHIAKI_LOGE(discovery->log, "Discovery failed to bind"); diff --git a/lib/src/discoveryservice.c b/lib/src/discoveryservice.c index 09f31ef..f163092 100644 --- a/lib/src/discoveryservice.c +++ b/lib/src/discoveryservice.c @@ -238,7 +238,7 @@ static void discovery_service_host_received(ChiakiDiscoveryHost *host, void *use if(service->hosts_count == service->options.hosts_max) { CHIAKI_LOGE(service->log, "Discovery Service received new host, but no space available"); - goto r2con; + goto rzcon; } CHIAKI_LOGI(service->log, "Discovery Service detected new host with id %s", host->host_id); @@ -279,7 +279,7 @@ static void discovery_service_host_received(ChiakiDiscoveryHost *host, void *use if(change) discovery_service_report_state(service); -r2con: +rzcon: chiaki_mutex_unlock(&service->state_mutex); } From acf15480f2b69ad0e61835a0d2a0e73030b490ab Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 11 Jan 2021 20:03:28 +0100 Subject: [PATCH 033/104] Add switch rumble and motion feedbacks --- switch/include/gui.h | 6 -- switch/include/host.h | 7 +- switch/include/io.h | 21 +++- switch/src/gui.cpp | 42 ++------ switch/src/host.cpp | 36 ++++++- switch/src/io.cpp | 235 ++++++++++++++++++++++++++++++++++-------- switch/src/main.cpp | 8 +- 7 files changed, 255 insertions(+), 100 deletions(-) diff --git a/switch/include/gui.h b/switch/include/gui.h index 38f13ec..033cf67 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -69,12 +69,6 @@ class PSRemotePlay : public brls::View // to send gamepad inputs Host *host; brls::Label *label; - ChiakiControllerState state = {0}; - // FPS calculation - // double base_time; - // int frame_counter = 0; - // int fps = 0; - public: PSRemotePlay(Host *host); ~PSRemotePlay(); diff --git a/switch/include/host.h b/switch/include/host.h index a93e7c9..1e8114d 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -54,7 +54,9 @@ class Host std::function chiaki_regist_event_type_finished_success = nullptr; std::function chiaki_event_connected_cb = nullptr; std::function chiaki_even_login_pin_request_cb = nullptr; + std::function chiaki_event_rumble_cb = nullptr; std::function chiaki_event_quit_cb = nullptr; + std::function io_read_controller_cb = nullptr; // internal state bool discovered = false; @@ -73,6 +75,7 @@ class Host std::string server_nickname; ChiakiTarget target = CHIAKI_TARGET_PS4_UNKNOWN; ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; + ChiakiControllerState controller_state = {0}; // mac address = 48 bits uint8_t server_mac[6] = {0}; @@ -96,7 +99,7 @@ class Host int FiniSession(); void StopSession(); void StartSession(); - void SendFeedbackState(ChiakiControllerState *); + void SendFeedbackState(); void RegistCB(ChiakiRegistEvent *); void ConnectionEventCB(ChiakiEvent *); bool GetVideoResolution(int *ret_width, int *ret_height); @@ -110,7 +113,9 @@ class Host void SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success); void SetEventConnectedCallback(std::function chiaki_event_connected_cb); void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb); + void SetEventRumbleCallback(std::function chiaki_event_rumble_cb); void SetEventQuitCallback(std::function chiaki_event_quit_cb); + void SetReadControllerCallback(std::function io_read_controller_cb); bool IsRegistered(); bool IsDiscovered(); bool IsReady(); diff --git a/switch/include/io.h b/switch/include/io.h index 85e2966..8466fb0 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -28,6 +28,12 @@ Omit khrplatform: False Reproducible: False */ +#ifdef __SWITCH__ +#include +#else +#include +#endif + #include extern "C" { @@ -65,6 +71,11 @@ class IO SDL_AudioDeviceID sdl_audio_device_id = 0; SDL_Event sdl_event; SDL_Joystick *sdl_joystick_ptr[SDL_JOYSTICK_COUNT] = {0}; +#ifdef __SWITCH__ + PadState pad; + HidSixAxisSensorHandle sixaxis_handles[4]; + HidVibrationDeviceHandle vibration_handles[2][2]; +#endif GLuint vao; GLuint vbo; GLuint tex[PLANES_COUNT]; @@ -86,7 +97,7 @@ class IO void SetOpenGlYUVPixels(AVFrame *frame); bool ReadGameKeys(SDL_Event *event, ChiakiControllerState *state); bool ReadGameTouchScreen(ChiakiControllerState *state); - + bool ReadGameSixAxis(ChiakiControllerState *state); public: // singleton configuration IO(const IO&) = delete; @@ -100,9 +111,11 @@ class IO void AudioCB(int16_t *buf, size_t samples_count); bool InitVideo(int video_width, int video_height, int screen_width, int screen_height); bool FreeVideo(); - bool InitJoystick(); - bool FreeJoystick(); - bool MainLoop(ChiakiControllerState *state); + bool InitController(); + bool FreeController(); + bool MainLoop(); + void UpdateControllerState(ChiakiControllerState *state); + void SetRumble(uint8_t left, uint8_t right); }; #endif //CHIAKI_IO_H diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index f31cba8..e77576c 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -42,6 +42,9 @@ HostInterface::HostInterface(Host *host) // when the host is connected this->host->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); this->host->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); + // allow host to update controller state + this->host->SetEventRumbleCallback(std::bind(&IO::SetRumble, this->io, std::placeholders::_1, std::placeholders::_2)); + this->host->SetReadControllerCallback(std::bind(&IO::UpdateControllerState, this->io, std::placeholders::_1)); } HostInterface::~HostInterface() @@ -245,7 +248,7 @@ MainApplication::MainApplication(DiscoveryManager *discoverymanager) MainApplication::~MainApplication() { this->discoverymanager->SetService(false); - //this->io->FreeJoystick(); + this->io->FreeController(); this->io->FreeVideo(); } @@ -264,16 +267,15 @@ bool MainApplication::Load() // init chiaki gl after borealis // let borealis manage the main screen/window - if(!io->InitVideo(0, 0, SCREEN_W, SCREEN_H)) { brls::Logger::error("Failed to initiate Video"); } - brls::Logger::info("Load sdl joysticks"); - if(!io->InitJoystick()) + brls::Logger::info("Load sdl/hid controller"); + if(!io->InitController()) { - brls::Logger::error("Faled to initiate Joysticks"); + brls::Logger::error("Faled to initiate Controller"); } // Create a view @@ -541,38 +543,12 @@ PSRemotePlay::PSRemotePlay(Host *host) : host(host) { this->io = IO::GetInstance(); - - // store joycon/touchpad keys - for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) - // start touchpad as "untouched" - this->state.touches[x].id = -1; - - // this->base_time=glfwGetTime(); } void PSRemotePlay::draw(NVGcontext *vg, int x, int y, unsigned width, unsigned height, brls::Style *style, brls::FrameContext *ctx) { - this->io->MainLoop(&this->state); - this->host->SendFeedbackState(&this->state); - - // FPS calculation - // this->frame_counter += 1; - // double frame_time = glfwGetTime(); - // if((frame_time - base_time) >= 1.0) - // { - // base_time += 1; - // //printf("FPS: %d\n", this->frame_counter); - // this->fps = this->frame_counter; - // this->frame_counter = 0; - // } - // nvgBeginPath(vg); - // nvgFillColor(vg, nvgRGBA(255,192,0,255)); - // nvgFontFaceId(vg, ctx->fontStash->regular); - // nvgFontSize(vg, style->Label.smallFontSize); - // nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); - // char fps_str[9] = {0}; - // sprintf(fps_str, "FPS: %000d", this->fps); - // nvgText(vg, 5,10, fps_str, NULL); + this->io->MainLoop(); + this->host->SendFeedbackState(); } PSRemotePlay::~PSRemotePlay() diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 50fdd2f..573a887 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -156,9 +156,17 @@ int Host::InitSession(IO *user) // audio setting_cb and frame_cb chiaki_opus_decoder_set_cb(&this->opus_decoder, InitAudioCB, AudioCB, user); chiaki_opus_decoder_get_sink(&this->opus_decoder, &audio_sink); - chiaki_session_set_audio_sink(&(this->session), &audio_sink); - chiaki_session_set_video_sample_cb(&(this->session), VideoCB, user); - chiaki_session_set_event_cb(&(this->session), EventCB, this); + chiaki_session_set_audio_sink(&this->session, &audio_sink); + chiaki_session_set_video_sample_cb(&this->session, VideoCB, user); + chiaki_session_set_event_cb(&this->session, EventCB, this); + + // init controller states + chiaki_controller_state_set_idle(&this->controller_state); + + for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) + // start touchpad as "untouched" + this->controller_state.touches[x].id = -1; + return 0; } @@ -189,10 +197,13 @@ void Host::StartSession() } } -void Host::SendFeedbackState(ChiakiControllerState *state) +void Host::SendFeedbackState() { // send controller/joystick key - chiaki_session_set_controller_state(&this->session, state); + if(this->io_read_controller_cb != nullptr) + this->io_read_controller_cb(&this->controller_state); + + chiaki_session_set_controller_state(&this->session, &this->controller_state); } void Host::ConnectionEventCB(ChiakiEvent *event) @@ -209,6 +220,11 @@ void Host::ConnectionEventCB(ChiakiEvent *event) if(this->chiaki_even_login_pin_request_cb != nullptr) this->chiaki_even_login_pin_request_cb(event->login_pin_request.pin_incorrect); break; + case CHIAKI_EVENT_RUMBLE: + CHIAKI_LOGD(this->log, "EventCB CHIAKI_EVENT_RUMBLE"); + if(this->chiaki_event_rumble_cb != nullptr) + this->chiaki_event_rumble_cb(event->rumble.left, event->rumble.right); + break; case CHIAKI_EVENT_QUIT: CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_QUIT"); if(this->chiaki_event_quit_cb != nullptr) @@ -352,6 +368,16 @@ void Host::SetEventQuitCallback(std::function chiaki_ev this->chiaki_event_quit_cb = chiaki_event_quit_cb; } +void Host::SetEventRumbleCallback(std::function chiaki_event_rumble_cb) +{ + this->chiaki_event_rumble_cb = chiaki_event_rumble_cb; +} + +void Host::SetReadControllerCallback(std::function io_read_controller_cb) +{ + this->io_read_controller_cb = io_read_controller_cb; +} + bool Host::IsRegistered() { return this->registered; diff --git a/switch/src/io.cpp b/switch/src/io.cpp index e086215..65df801 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -1,11 +1,5 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL -#ifdef __SWITCH__ -#include -#else -#include -#endif - #include "io.h" #include "settings.h" @@ -345,22 +339,23 @@ bool IO::FreeVideo() return ret; } -bool IO::ReadGameTouchScreen(ChiakiControllerState *state) +bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) { #ifdef __SWITCH__ - hidScanInput(); - int touch_count = hidTouchCount(); + HidTouchScreenState sw_state = {0}; + bool ret = false; - if(!touch_count) + if(!hidGetTouchScreenStates(&sw_state, 1) || sw_state.count == 0) { + // flush state + chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { - if(state->touches[i].id != -1) + if(chiaki_state->touches[i].id != -1) { - state->touches[i].x = 0; - state->touches[i].y = 0; - state->touches[i].id = -1; - state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release + chiaki_state->touches[i].x = 0; + chiaki_state->touches[i].y = 0; + chiaki_state->touches[i].id = -1; // the state changed ret = true; } @@ -368,32 +363,25 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) return ret; } - touchPosition touch; - for(int i = 0; i < touch_count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + // scale switch screen to the PS trackpad + for(int i = 0; i < sw_state.count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { - hidTouchRead(&touch, i); // 1280×720 px (16:9) // ps4 controller aspect ratio looks closer to 29:10 - uint16_t x = touch.px * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); - uint16_t y = touch.py * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); + uint16_t x = sw_state.touches[i].x * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = sw_state.touches[i].y * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); // use nintendo switch border's 5% to if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) - { - state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - // printf("CHIAKI_CONTROLLER_BUTTON_TOUCHPAD\n"); - } + chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen else - { - state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - } + chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - state->touches[i].x = x; - state->touches[i].y = y; - state->touches[i].id = i; - // printf("[point_id=%d] px=%03d, py=%03d, dx=%03d, dy=%03d, angle=%03d\n", - // i, touch.px, touch.py, touch.dx, touch.dy, touch.angle); + chiaki_state->touches[i].x = x; + chiaki_state->touches[i].y = y; + chiaki_state->touches[i].id = i; + // printf("[point_id=%d] x=%03d, y=%03d\n", i, x, y); ret = true; } return ret; @@ -402,14 +390,127 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) #endif } +void IO::SetRumble(uint8_t left, uint8_t right) +{ +#ifdef __SWITCH__ + Result rc = 0; + HidVibrationValue vibration_values[] = { + { + .amp_low = 0.0f, + .freq_low = 160.0f, + .amp_high = 0.0f, + .freq_high = 320.0f, + }, + { + .amp_low = 0.0f, + .freq_low = 160.0f, + .amp_high = 0.0f, + .freq_high = 320.0f, + }}; + + int target_device = padIsHandheld(&pad) ? 0 : 1; + if(left > 0) + { + // SDL_HapticRumblePlay(this->sdl_haptic_ptr[0], left / 100, 5000); + vibration_values[0].amp_low = (float)left / (float)100; + vibration_values[0].amp_high = (float)left / (float)100; + vibration_values[0].freq_low *= (float)left / (float)100; + vibration_values[0].freq_high *= (float)left / (float)100; + } + + if(right > 0) + { + // SDL_HapticRumblePlay(this->sdl_haptic_ptr[1], right / 100, 5000); + vibration_values[1].amp_low = (float)right / (float)100; + vibration_values[1].amp_high = (float)right / (float)100; + vibration_values[1].freq_low *= (float)left / (float)100; + vibration_values[1].freq_high *= (float)left / (float)100; + } + + // printf("left ptr %p amp_low %f amp_high %f freq_low %f freq_high %f\n", + // &vibration_values[0], + // vibration_values[0].amp_low, + // vibration_values[0].amp_high, + // vibration_values[0].freq_low, + // vibration_values[0].freq_high); + + // printf("right ptr %p amp_low %f amp_high %f freq_low %f freq_high %f\n", + // &vibration_values[1], + // vibration_values[1].amp_low, + // vibration_values[1].amp_high, + // vibration_values[1].freq_low, + // vibration_values[1].freq_high); + + rc = hidSendVibrationValues(this->vibration_handles[target_device], vibration_values, 2); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidSendVibrationValues() returned: 0x%x", rc); + +#endif +} + +bool IO::ReadGameSixAxis(ChiakiControllerState *state) +{ +#ifdef __SWITCH__ + // Read from the correct sixaxis handle depending on the current input style + HidSixAxisSensorState sixaxis = {0}; + uint64_t style_set = padGetStyleSet(&pad); + if(style_set & HidNpadStyleTag_NpadHandheld) + hidGetSixAxisSensorStates(this->sixaxis_handles[0], &sixaxis, 1); + else if(style_set & HidNpadStyleTag_NpadFullKey) + hidGetSixAxisSensorStates(this->sixaxis_handles[1], &sixaxis, 1); + else if(style_set & HidNpadStyleTag_NpadJoyDual) + { + // For JoyDual, read from either the Left or Right Joy-Con depending on which is/are connected + u64 attrib = padGetAttributes(&pad); + if(attrib & HidNpadAttribute_IsLeftConnected) + hidGetSixAxisSensorStates(this->sixaxis_handles[2], &sixaxis, 1); + else if(attrib & HidNpadAttribute_IsRightConnected) + hidGetSixAxisSensorStates(this->sixaxis_handles[3], &sixaxis, 1); + } + + // printf("Acceleration: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.acceleration.x, sixaxis.acceleration.y, sixaxis.acceleration.z); + // printf("Angular velocity: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angular_velocity.x, sixaxis.angular_velocity.y, sixaxis.angular_velocity.z); + // printf("Angle: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angle.x, sixaxis.angle.y, sixaxis.angle.z); + // printf("Direction matrix:\n" + // " [ % .4f, % .4f, % .4f ]\n" + // " [ % .4f, % .4f, % .4f ]\n" + // " [ % .4f, % .4f, % .4f ]\n", + // sixaxis.direction.direction[0][0], sixaxis.direction.direction[1][0], sixaxis.direction.direction[2][0], + // sixaxis.direction.direction[0][1], sixaxis.direction.direction[1][1], sixaxis.direction.direction[2][1], + // sixaxis.direction.direction[0][2], sixaxis.direction.direction[1][2], sixaxis.direction.direction[2][2]); + + state->gyro_x = sixaxis.angular_velocity.x; + state->gyro_y = sixaxis.angular_velocity.y; + state->gyro_z = sixaxis.angular_velocity.z; + state->accel_x = sixaxis.acceleration.x; + state->accel_y = sixaxis.acceleration.y; + state->accel_z = sixaxis.acceleration.z; + + // thank you @thestr4ng3r for the hint + // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Euler_angles_to_quaternion_conversion + double cy = cos(sixaxis.angle.z * 0.5); + double sy = sin(sixaxis.angle.z * 0.5); + double cp = cos(sixaxis.angle.y * 0.5); + double sp = sin(sixaxis.angle.y * 0.5); + double cr = cos(sixaxis.angle.x * 0.5); + double sr = sin(sixaxis.angle.x * 0.5); + + state->orient_x = sr * cp * cy - cr * sp * sy; + state->orient_y = cr * sp * cy + sr * cp * sy; + state->orient_z = cr * cp * sy - sr * sp * cy; + state->orient_w = cr * cp * cy + sr * sp * sy; + return true; +#else + return false; +#endif +} + bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) { // return true if an event changed (gamepad input) // TODO // share vs PS button - // Gyro ? - // rumble ? bool ret = true; switch(event->type) { @@ -620,7 +721,7 @@ bool IO::InitOpenGlTextures() D(glGenTextures(PLANES_COUNT, this->tex)); D(glGenBuffers(PLANES_COUNT, this->pbo)); - uint8_t uv_default[] = { 0x7f, 0x7f }; + uint8_t uv_default[] = {0x7f, 0x7f}; for(int i = 0; i < PLANES_COUNT; i++) { D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); @@ -633,7 +734,7 @@ bool IO::InitOpenGlTextures() D(glUseProgram(this->prog)); // bind only as many planes as we need - const char *plane_names[] = { "plane1", "plane2", "plane3" }; + const char *plane_names[] = {"plane1", "plane2", "plane3"}; for(int i = 0; i < PLANES_COUNT; i++) D(glUniform1i(glGetUniformLocation(this->prog, plane_names[i]), i)); @@ -786,7 +887,7 @@ inline void IO::OpenGlDraw() D(glFinish()); } -bool IO::InitJoystick() +bool IO::InitController() { // https://github.com/switchbrew/switch-examples/blob/master/graphics/sdl2/sdl2-simple/source/main.cpp#L57 // open CONTROLLER_PLAYER_1 and CONTROLLER_PLAYER_2 @@ -800,24 +901,64 @@ bool IO::InitJoystick() CHIAKI_LOGE(this->log, "SDL_JoystickOpen: %s\n", SDL_GetError()); return false; } + // this->sdl_haptic_ptr[i] = SDL_HapticOpenFromJoystick(sdl_joystick_ptr[i]); + // SDL_HapticRumbleInit(this->sdl_haptic_ptr[i]); + // if(sdl_haptic_ptr[i] == nullptr) + // { + // CHIAKI_LOGE(this->log, "SDL_HapticRumbleInit: %s\n", SDL_GetError()); + // } } +#ifdef __SWITCH__ +Result rc = 0; + // Configure our supported input layout: a single player with standard controller styles + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + + // Initialize the default gamepad (which reads handheld mode inputs as well as the first connected controller) + padInitializeDefault(&this->pad); + // touchpad + hidInitializeTouchScreen(); + // It's necessary to initialize these separately as they all have different handle values + hidGetSixAxisSensorHandles(&this->sixaxis_handles[0], 1, HidNpadIdType_Handheld, HidNpadStyleTag_NpadHandheld); + hidGetSixAxisSensorHandles(&this->sixaxis_handles[1], 1, HidNpadIdType_No1, HidNpadStyleTag_NpadFullKey); + hidGetSixAxisSensorHandles(&this->sixaxis_handles[2], 2, HidNpadIdType_No1, HidNpadStyleTag_NpadJoyDual); + hidStartSixAxisSensor(this->sixaxis_handles[0]); + hidStartSixAxisSensor(this->sixaxis_handles[1]); + hidStartSixAxisSensor(this->sixaxis_handles[2]); + hidStartSixAxisSensor(this->sixaxis_handles[3]); + + rc = hidInitializeVibrationDevices(this->vibration_handles[0], 2, HidNpadIdType_Handheld, HidNpadStyleTag_NpadHandheld); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidInitializeVibrationDevices() HidNpadIdType_Handheld returned: 0x%x", rc); + + rc = hidInitializeVibrationDevices(this->vibration_handles[1], 2, HidNpadIdType_No1, HidNpadStyleTag_NpadJoyDual); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidInitializeVibrationDevices() HidNpadIdType_No1 returned: 0x%x", rc); + +#endif return true; } -bool IO::FreeJoystick() +bool IO::FreeController() { for(int i = 0; i < SDL_JOYSTICK_COUNT; i++) { - if(SDL_JoystickGetAttached(sdl_joystick_ptr[i])) - SDL_JoystickClose(sdl_joystick_ptr[i]); + SDL_JoystickClose(this->sdl_joystick_ptr[i]); + // SDL_HapticClose(this->sdl_haptic_ptr[i]); } +#ifdef __SWITCH__ + hidStopSixAxisSensor(this->sixaxis_handles[0]); + hidStopSixAxisSensor(this->sixaxis_handles[1]); + hidStopSixAxisSensor(this->sixaxis_handles[2]); + hidStopSixAxisSensor(this->sixaxis_handles[3]); +#endif return true; } -bool IO::MainLoop(ChiakiControllerState *state) +void IO::UpdateControllerState(ChiakiControllerState *state) { - D(glUseProgram(this->prog)); - +#ifdef __SWITCH__ + padUpdate(&this->pad); +#endif // handle SDL events while(SDL_PollEvent(&this->sdl_event)) { @@ -825,11 +966,17 @@ bool IO::MainLoop(ChiakiControllerState *state) switch(this->sdl_event.type) { case SDL_QUIT: - return false; + this->quit = true; } } ReadGameTouchScreen(state); + ReadGameSixAxis(state); +} + +bool IO::MainLoop() +{ + D(glUseProgram(this->prog)); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 84f9210..34e4b20 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -83,12 +83,6 @@ extern "C" void userAppInit() // load socket custom config socketInitialize(&g_chiakiSocketInitConfig); setsysInitialize(); - - // padConfigureInput(1, HidNpadStyleSet_NpadStandard); - // PadState pad; - // padInitializeDefault(&pad); - - //hidInitializeTouchScreen(); } extern "C" void userAppExit() @@ -126,7 +120,7 @@ int main(int argc, char *argv[]) return 1; } - CHIAKI_LOGI(log, "Loading SDL audio / joystick"); + CHIAKI_LOGI(log, "Loading SDL audio / joystick / haptic"); if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_JOYSTICK)) { CHIAKI_LOGE(log, "SDL initialization failed: %s", SDL_GetError()); From 12054a91c9f98e88bd05986eadaeeddfacc106dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 11 Jan 2021 21:22:04 +0100 Subject: [PATCH 034/104] Fix Motion Data on Switch --- switch/src/io.cpp | 76 ++++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 65df801..541c0de 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -468,37 +468,53 @@ bool IO::ReadGameSixAxis(ChiakiControllerState *state) hidGetSixAxisSensorStates(this->sixaxis_handles[3], &sixaxis, 1); } - // printf("Acceleration: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.acceleration.x, sixaxis.acceleration.y, sixaxis.acceleration.z); - // printf("Angular velocity: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angular_velocity.x, sixaxis.angular_velocity.y, sixaxis.angular_velocity.z); - // printf("Angle: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angle.x, sixaxis.angle.y, sixaxis.angle.z); - // printf("Direction matrix:\n" - // " [ % .4f, % .4f, % .4f ]\n" - // " [ % .4f, % .4f, % .4f ]\n" - // " [ % .4f, % .4f, % .4f ]\n", - // sixaxis.direction.direction[0][0], sixaxis.direction.direction[1][0], sixaxis.direction.direction[2][0], - // sixaxis.direction.direction[0][1], sixaxis.direction.direction[1][1], sixaxis.direction.direction[2][1], - // sixaxis.direction.direction[0][2], sixaxis.direction.direction[1][2], sixaxis.direction.direction[2][2]); + state->gyro_x = sixaxis.angular_velocity.x * 2.0f * M_PI; + state->gyro_y = sixaxis.angular_velocity.z * 2.0f * M_PI; + state->gyro_z = -sixaxis.angular_velocity.y * 2.0f * M_PI; + state->accel_x = -sixaxis.acceleration.x; + state->accel_y = -sixaxis.acceleration.z; + state->accel_z = sixaxis.acceleration.y; - state->gyro_x = sixaxis.angular_velocity.x; - state->gyro_y = sixaxis.angular_velocity.y; - state->gyro_z = sixaxis.angular_velocity.z; - state->accel_x = sixaxis.acceleration.x; - state->accel_y = sixaxis.acceleration.y; - state->accel_z = sixaxis.acceleration.z; - - // thank you @thestr4ng3r for the hint - // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Euler_angles_to_quaternion_conversion - double cy = cos(sixaxis.angle.z * 0.5); - double sy = sin(sixaxis.angle.z * 0.5); - double cp = cos(sixaxis.angle.y * 0.5); - double sp = sin(sixaxis.angle.y * 0.5); - double cr = cos(sixaxis.angle.x * 0.5); - double sr = sin(sixaxis.angle.x * 0.5); - - state->orient_x = sr * cp * cy - cr * sp * sy; - state->orient_y = cr * sp * cy + sr * cp * sy; - state->orient_z = cr * cp * sy - sr * sp * cy; - state->orient_w = cr * cp * cy + sr * sp * sy; + // https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2015/01/matrix-to-quat.pdf + float (*dm)[3] = sixaxis.direction.direction; + float m[3][3] = { + { dm[0][0], dm[2][0], dm[1][0] }, + { dm[0][2], dm[2][2], dm[1][2] }, + { dm[0][1], dm[2][1], dm[1][1] } + }; + std::array q; + float t; + if(m[2][2] < 0) + { + if (m[0][0] > m[1][1]) + { + t = 1 + m[0][0] - m[1][1] - m[2][2]; + q = { t, m[0][1] + m[1][0], m[2][0] + m[0][2], m[1][2] - m[2][1] }; + } + else + { + t = 1 - m[0][0] + m[1][1] -m[2][2]; + q = { m[0][1] + m[1][0], t, m[1][2] + m[2][1], m[2][0] - m[0][2] }; + } + } + else + { + if(m[0][0] < -m[1][1]) + { + t = 1 - m[0][0] - m[1][1] + m[2][2]; + q = { m[2][0] + m[0][2], m[1][2] + m[2][1], t, m[0][1] - m[1][0] }; + } + else + { + t = 1 + m[0][0] + m[1][1] + m[2][2]; + q = { m[1][2] - m[2][1], m[2][0] - m[0][2], m[0][1] - m[1][0], t }; + } + } + float fac = 0.5f / sqrt(t); + state->orient_x = q[0] * fac; + state->orient_y = q[1] * fac; + state->orient_z = -q[2] * fac; + state->orient_w = q[3] * fac; return true; #else return false; From 1b8fa556f88399fdd72c0db5e97f18891741d398 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 11 Jan 2021 22:10:51 +0100 Subject: [PATCH 035/104] Fix switch touchpad resolution --- switch/src/io.cpp | 55 ++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 541c0de..2b4dab5 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -345,43 +345,34 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) HidTouchScreenState sw_state = {0}; bool ret = false; - if(!hidGetTouchScreenStates(&sw_state, 1) || sw_state.count == 0) - { - // flush state - chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) - { - if(chiaki_state->touches[i].id != -1) - { - chiaki_state->touches[i].x = 0; - chiaki_state->touches[i].y = 0; - chiaki_state->touches[i].id = -1; - // the state changed - ret = true; - } - } - return ret; - } - + hidGetTouchScreenStates(&sw_state, 1); // scale switch screen to the PS trackpad - for(int i = 0; i < sw_state.count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release + for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { + if((i + 1) <= sw_state.count) + { + uint16_t x = sw_state.touches[i].x * ((float)DS4_TRACKPAD_MAX_X / (float)SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = sw_state.touches[i].y * ((float)DS4_TRACKPAD_MAX_Y / (float)SWITCH_TOUCHSCREEN_MAX_Y); - // 1280×720 px (16:9) - // ps4 controller aspect ratio looks closer to 29:10 - uint16_t x = sw_state.touches[i].x * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); - uint16_t y = sw_state.touches[i].y * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); + // use nintendo switch border's 5% to trigger the touchpad button + if(x <= (DS4_TRACKPAD_MAX_X * 0.05) || x >= (DS4_TRACKPAD_MAX_X * 0.95) || y <= (DS4_TRACKPAD_MAX_Y * 0.05) || y >= (DS4_TRACKPAD_MAX_Y * 0.95)) + chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - // use nintendo switch border's 5% to - if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) - chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen + chiaki_state->touches[i].x = x; + chiaki_state->touches[i].y = y; + chiaki_state->touches[i].id = i; + } else - chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - - chiaki_state->touches[i].x = x; - chiaki_state->touches[i].y = y; - chiaki_state->touches[i].id = i; - // printf("[point_id=%d] x=%03d, y=%03d\n", i, x, y); + { + // flush touch state + chiaki_state->touches[i].x = 0; + chiaki_state->touches[i].y = 0; + chiaki_state->touches[i].id = -1; + } + // printf("switch id=%d x=%03d, y=%03d\nchiaki id=%d x=%03d, y=%03d\n", + // i, sw_state.touches[i].x, sw_state.touches[i].y, + // chiaki_state->touches[i].id, chiaki_state->touches[i].x, chiaki_state->touches[i].y); ret = true; } return ret; From fc58e83e9cd75c0eb8b4d7afd9d910c0f13945b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 12:23:02 +0100 Subject: [PATCH 036/104] Track Finger IDs on Switch --- switch/include/host.h | 5 ++-- switch/include/io.h | 6 +++-- switch/src/gui.cpp | 2 +- switch/src/host.cpp | 8 ++---- switch/src/io.cpp | 59 ++++++++++++++++++++++++++----------------- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/switch/include/host.h b/switch/include/host.h index 1e8114d..e28183e 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -56,7 +56,7 @@ class Host std::function chiaki_even_login_pin_request_cb = nullptr; std::function chiaki_event_rumble_cb = nullptr; std::function chiaki_event_quit_cb = nullptr; - std::function io_read_controller_cb = nullptr; + std::function *)> io_read_controller_cb = nullptr; // internal state bool discovered = false; @@ -76,6 +76,7 @@ class Host ChiakiTarget target = CHIAKI_TARGET_PS4_UNKNOWN; ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; ChiakiControllerState controller_state = {0}; + std::map finger_id_touch_id; // mac address = 48 bits uint8_t server_mac[6] = {0}; @@ -115,7 +116,7 @@ class Host void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb); void SetEventRumbleCallback(std::function chiaki_event_rumble_cb); void SetEventQuitCallback(std::function chiaki_event_quit_cb); - void SetReadControllerCallback(std::function io_read_controller_cb); + void SetReadControllerCallback(std::function *)> io_read_controller_cb); bool IsRegistered(); bool IsDiscovered(); bool IsReady(); diff --git a/switch/include/io.h b/switch/include/io.h index 8466fb0..3c1031d 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -35,6 +35,8 @@ Reproducible: False #endif #include +#include + extern "C" { #include @@ -96,7 +98,7 @@ class IO GLuint CreateAndCompileShader(GLenum type, const char *source); void SetOpenGlYUVPixels(AVFrame *frame); bool ReadGameKeys(SDL_Event *event, ChiakiControllerState *state); - bool ReadGameTouchScreen(ChiakiControllerState *state); + bool ReadGameTouchScreen(ChiakiControllerState *state, std::map *finger_id_touch_id); bool ReadGameSixAxis(ChiakiControllerState *state); public: // singleton configuration @@ -114,7 +116,7 @@ class IO bool InitController(); bool FreeController(); bool MainLoop(); - void UpdateControllerState(ChiakiControllerState *state); + void UpdateControllerState(ChiakiControllerState *state, std::map *finger_id_touch_id); void SetRumble(uint8_t left, uint8_t right); }; diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index e77576c..2d0b71f 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -44,7 +44,7 @@ HostInterface::HostInterface(Host *host) this->host->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); // allow host to update controller state this->host->SetEventRumbleCallback(std::bind(&IO::SetRumble, this->io, std::placeholders::_1, std::placeholders::_2)); - this->host->SetReadControllerCallback(std::bind(&IO::UpdateControllerState, this->io, std::placeholders::_1)); + this->host->SetReadControllerCallback(std::bind(&IO::UpdateControllerState, this->io, std::placeholders::_1, std::placeholders::_2)); } HostInterface::~HostInterface() diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 573a887..2f50e2f 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -163,10 +163,6 @@ int Host::InitSession(IO *user) // init controller states chiaki_controller_state_set_idle(&this->controller_state); - for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) - // start touchpad as "untouched" - this->controller_state.touches[x].id = -1; - return 0; } @@ -201,7 +197,7 @@ void Host::SendFeedbackState() { // send controller/joystick key if(this->io_read_controller_cb != nullptr) - this->io_read_controller_cb(&this->controller_state); + this->io_read_controller_cb(&this->controller_state, &finger_id_touch_id); chiaki_session_set_controller_state(&this->session, &this->controller_state); } @@ -373,7 +369,7 @@ void Host::SetEventRumbleCallback(std::function chiaki_e this->chiaki_event_rumble_cb = chiaki_event_rumble_cb; } -void Host::SetReadControllerCallback(std::function io_read_controller_cb) +void Host::SetReadControllerCallback(std::function *)> io_read_controller_cb) { this->io_read_controller_cb = io_read_controller_cb; } diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 2b4dab5..409bba0 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -339,7 +339,7 @@ bool IO::FreeVideo() return ret; } -bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) +bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state, std::map *finger_id_touch_id) { #ifdef __SWITCH__ HidTouchScreenState sw_state = {0}; @@ -348,31 +348,44 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) hidGetTouchScreenStates(&sw_state, 1); // scale switch screen to the PS trackpad chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + + // un-touch all old touches + for(auto it = finger_id_touch_id->begin(); it != finger_id_touch_id->end();) { - if((i + 1) <= sw_state.count) + auto cur = it; + it++; + for(int i = 0; i < sw_state.count; i++) { - uint16_t x = sw_state.touches[i].x * ((float)DS4_TRACKPAD_MAX_X / (float)SWITCH_TOUCHSCREEN_MAX_X); - uint16_t y = sw_state.touches[i].y * ((float)DS4_TRACKPAD_MAX_Y / (float)SWITCH_TOUCHSCREEN_MAX_Y); - - // use nintendo switch border's 5% to trigger the touchpad button - if(x <= (DS4_TRACKPAD_MAX_X * 0.05) || x >= (DS4_TRACKPAD_MAX_X * 0.95) || y <= (DS4_TRACKPAD_MAX_Y * 0.05) || y >= (DS4_TRACKPAD_MAX_Y * 0.95)) - chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - - chiaki_state->touches[i].x = x; - chiaki_state->touches[i].y = y; - chiaki_state->touches[i].id = i; + if(sw_state.touches[i].finger_id == cur->first) + goto cont; } - else + if(cur->second >= 0) + chiaki_controller_state_stop_touch(chiaki_state, (uint8_t)cur->second); + finger_id_touch_id->erase(cur); +cont: + continue; + } + + + // touch or update all current touches + for(int i = 0; i < sw_state.count; i++) + { + uint16_t x = sw_state.touches[i].x * ((float)DS4_TRACKPAD_MAX_X / (float)SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = sw_state.touches[i].y * ((float)DS4_TRACKPAD_MAX_Y / (float)SWITCH_TOUCHSCREEN_MAX_Y); + // use nintendo switch border's 5% to trigger the touchpad button + if(x <= (DS4_TRACKPAD_MAX_X * 0.05) || x >= (DS4_TRACKPAD_MAX_X * 0.95) || y <= (DS4_TRACKPAD_MAX_Y * 0.05) || y >= (DS4_TRACKPAD_MAX_Y * 0.95)) + chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen + + auto it = finger_id_touch_id->find(sw_state.touches[i].finger_id); + if(it == finger_id_touch_id->end()) { - // flush touch state - chiaki_state->touches[i].x = 0; - chiaki_state->touches[i].y = 0; - chiaki_state->touches[i].id = -1; + // new touch + (*finger_id_touch_id)[sw_state.touches[i].finger_id] = + chiaki_controller_state_start_touch(chiaki_state, x, y); } - // printf("switch id=%d x=%03d, y=%03d\nchiaki id=%d x=%03d, y=%03d\n", - // i, sw_state.touches[i].x, sw_state.touches[i].y, - // chiaki_state->touches[i].id, chiaki_state->touches[i].x, chiaki_state->touches[i].y); + else if(it->second >= 0) + chiaki_controller_state_set_touch_pos(chiaki_state, (uint8_t)it->second, x, y); + // it->second < 0 ==> touch ignored because there were already too many multi-touches ret = true; } return ret; @@ -961,7 +974,7 @@ bool IO::FreeController() return true; } -void IO::UpdateControllerState(ChiakiControllerState *state) +void IO::UpdateControllerState(ChiakiControllerState *state, std::map *finger_id_touch_id) { #ifdef __SWITCH__ padUpdate(&this->pad); @@ -977,7 +990,7 @@ void IO::UpdateControllerState(ChiakiControllerState *state) } } - ReadGameTouchScreen(state); + ReadGameTouchScreen(state, finger_id_touch_id); ReadGameSixAxis(state); } From 0b6e479e0bafb2b1fdeb0832eb886cb78a8388b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 12:59:04 +0100 Subject: [PATCH 037/104] Fix some Double Free and uninitialized Memory in Switch --- switch/include/discoverymanager.h | 2 +- switch/src/discoverymanager.cpp | 6 ------ switch/src/main.cpp | 7 +++++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/switch/include/discoverymanager.h b/switch/include/discoverymanager.h index 76c504d..49c8dcc 100644 --- a/switch/include/discoverymanager.h +++ b/switch/include/discoverymanager.h @@ -24,7 +24,7 @@ class DiscoveryManager struct sockaddr * host_addr = nullptr; size_t host_addr_len = 0; uint32_t GetIPv4BroadcastAddr(); - bool service_enable; + bool service_enable = false; public: typedef enum hoststate diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index e9abe5f..7fa7122 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -35,19 +35,13 @@ DiscoveryManager::~DiscoveryManager() { // join discovery thread if(this->service_enable) - { SetService(false); - } - - chiaki_discovery_fini(&this->discovery); } void DiscoveryManager::SetService(bool enable) { if(this->service_enable == enable) - { return; - } this->service_enable = enable; diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 34e4b20..6b7c95e 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -129,8 +129,11 @@ int main(int argc, char *argv[]) // build sdl OpenGl and AV decoders graphical interface DiscoveryManager discoverymanager = DiscoveryManager(); - MainApplication app = MainApplication(&discoverymanager); - app.Load(); + { + // scope to delete MainApplication before SDL_Quit() + MainApplication app(&discoverymanager); + app.Load(); + } CHIAKI_LOGI(log, "Quit applet"); SDL_Quit(); From bb4e5398b289a1b6d6a301fe2278ffbe74e895e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 13:35:40 +0100 Subject: [PATCH 038/104] Update Switch Metadata and Icons --- switch/CMakeLists.txt | 8 ++++---- switch/nro_icon.jpg | Bin 0 -> 20589 bytes switch/nro_icon.png | Bin 0 -> 14350 bytes switch/res/icon.jpg | Bin 25090 -> 0 bytes switch/res/icon.png | Bin 0 -> 7791 bytes switch/src/gui.cpp | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 switch/nro_icon.jpg create mode 100644 switch/nro_icon.png delete mode 100644 switch/res/icon.jpg create mode 100644 switch/res/icon.png diff --git a/switch/CMakeLists.txt b/switch/CMakeLists.txt index 89bd789..4916a2e 100644 --- a/switch/CMakeLists.txt +++ b/switch/CMakeLists.txt @@ -126,9 +126,9 @@ install(TARGETS chiaki-borealis if(CHIAKI_IS_SWITCH) add_nro_target(chiaki chiaki-borealis - "chiaki" - "Chiaki team" + "Chiaki" + "H0neyBadger and thestr4ng3r" "${CHIAKI_VERSION}" - "${CMAKE_SOURCE_DIR}/switch/res/icon.jpg" - "${CMAKE_SOURCE_DIR}/switch/res") + "${CMAKE_CURRENT_SOURCE_DIR}/nro_icon.jpg" + "${CMAKE_CURRENT_SOURCE_DIR}/res") endif() diff --git a/switch/nro_icon.jpg b/switch/nro_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ed61ec2d18a7af214198cddada4fbb1bdb5df4b GIT binary patch literal 20589 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 literal 0 HcmV?d00001 diff --git a/switch/nro_icon.png b/switch/nro_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..65d745e017c63890d92698a06a862cccf62a7f5b GIT binary patch literal 14350 zcmeHtby(Ev+U`(8w+Iq4fRxnG-Q8W1!!R_=07Ewjh#(@M(jg_GpmazGf=UdCbazOD z0#avit-aRT=i6t0XYX^q|Mqar#qWLJ`?=recR$aY*Tm`Rs1g&<5&!@IVs$ko0{{R6 z{fGg;1ET*~dcAZ20LV21jm(e+5I+#Y3+~|T1_L1j5HJwT-x=NLKe?UZ>Pyd=5_47k z&>ZtPd7{w?Llmk3)voUO%QaBO>ATO!)c&3`4I^;(b8N(9UD7&3NwSWR8uaE}Z)GG9an` z%8@DXUW4vK&ypl56#oZD%uk_Fno*(=YwhYkc=u&I`x1M`wD3MwE-uUVeZFuuY1Pmu zLwWW@pgJmcE8C=Qf3A$$^>`pVE&l3QGE8C?swT_3wtY-$!) z#!0_kW(fHF1CL!}Q{9jyJ^qL-vHQ63e$ZSfyk23cn^sH^2qZKi7|3Z|+Fy}dGy4=f zKNVDwW?J*vj`o>&&?+|t#efCFQ@Z*3p2xD!#*~1)#NN<|PL$&V^Y`U-6^UQ8k&9jL zzYV*}9|;4P7H6YB-B$tz6PMnnZRz4*BI{8J1!C2vd1V!XQYoIAcV`)}it|jqYgBnJ zE&A+)9$2lWq%MqKy5XEuPE@5Yn@lGxq;a<~)gV6AB=DB-h^6K$f6BYBZL)nPpATeI2g?}7jp#Ij-{9DxSUf2h5Hdd`r->U z;$FFL`tGj93_GRkT1|wwjklV<63h|LFUi@ekpI#Hp^CSlcr1lgGgu3b@2<_K9wnWu zGyH~B6#ks3>NgNymRZE3UURRc|Kzb7A5XS^m!dL{rru}YV{;Bp1fryxYi99nLP5Sz zbSK`uXO!p3Bt88KPhzbt#IYmCi>nuxSDU?>!M4LqulR+jh+mH-5nhpnq3Etq)-!xE zNn(C>4gy6_2}(vqQlDkMVoWL+mE+JXELeXM2dovB%<#$@#ULAuoP=aF-Mss%cHu1f z7H){|J#tL2$##Bn_GIIWr_y)vBc`KmkKYNrlNyx?WmwFhhJSX;$9x^4G4NJ*I{Jro zlc@opmZD~niplrFj6olEP}FF3YM!G53%jCJzRO&4%vxcY_?M5hZ>8|55HoL8#BbP2 zL_aAV3&~mzX@AKO6R9d3L6Kr_UV9R9bLEAW*D|Hr<(rD1?&({a;;|WbELw+MZ-Bl; zQ~AWx^_IXDa=36I{jfe(VH`P$u6OAH&L$|^^c!CWx5w*PNJ63t<==tB#}rarzSZ10 z&KCYU;&_Lsx=yDC$}ML?a%#kE5ctvPD>mDOr^-o$PTGjZ6BKJ?wnPjjm5`eTrI69x zKonR-MLAlULi~9F$gBDp+uKAtGjWVJ?^(w0oVro`0A}e3m`4OG_&Nbu_jvT?)d=iZ z*r}f3rM={)mR2LR&EF;Q#*w@?QyL`U%lcC!@TnXYVLi)yv!SaNSgt##{rSq%Jzh^N zJdS22DtD38y`+Z~TRAZbQ#JP}UyYs5CJ{fibdo4mn0R_~6g?yC1X#{_l4LNnNG7nm z7vmfU)J`&utPmA9&kx9FVCHt%%Y91=)#dWPTehHQmhq+kj?7nI6`NB$5ln|7{Y}do z^^@@IO?Q9)?U!mY>jE)r+}XE`HyOBV2{xn<^R&1=;fwv$ZYAM7iga0N0(${YQd?$x z&fON;&dxZkqpw(!yUGp#$LY&o6(D_`dYFO=4BeTxLxnW*n?(mh<}y;wPBufO_C&4o zUpfmg?EwjyN_Fz8awdmeczWC#f~i&Sefxk)UT|;X+7*g*2(v}DhZ9WF(F@pm;n8$; zL^g*nTa)uMZET($X=ipQnm?^0OvOv+*#wqmcb{1~Dy zo@iQCX_KZgI!1CN9v3LeFA^I#^#XTXu!Iu z2laiDWDBkQX5enzptY?hMdSDjqDhX4N16gijQM1IKIFLz&OR%Ppksar(Q3j`_`TvM zdYyE8Kx!@l=xqxziptL5lHqpHlF=Q!NO|3^RS1_x7w>Wg6Q~fq4Tp1t?o5*33v)lN zC-2K2+uMC+boKUrn@2>i|NUMj@Ls{(%bfN2BKie}F-l{J0b~+uxlwYFmf5GfY1vZm z9bJ4)?-0e4RVDTJ@!pO5yxA4~fzto6z}wmF=W+TIt(FEq7;R=A<6bVgirn3OR*{iR zVP5uTWF9>5RsRsdc-%ECTz(iuz0y_zQYu2)`Wye^yBH7As|gO0g$KM0AFeI^;zSNr0d`j_7JDPvyK#Q87;|c4su!5D=#{P%99jkJ4{vBdV;TVYgPM*d@LwOyDKevGlx@;p!`eX236l>Gc%8g4~fc@ z;7krFhs_iP69h@U5;3^`1D}!u_%;VU5h|V9BUeKaUhDcgo3B}_(h?VLV$ud^1icUx z(ZmuXojo=o5If1Vhy;^h3k?)*9odnm^KU0^`xl>=2>LP>Cp?tnON|`@s;?E$zUC?I zbW?xM_neOY#p494!tRo6N!bcczt_*r393RI0~?|d3AF!{a@;+S7S#qL=+QVS9T_5pbH=*96p2@heAng59)!IYxi?-72IL!CGb{*+LdI`%a?gg|9w&#l!VDdD%<(=ieNbmZ} z9qgg*=~GqfeauM{)po(^8g`_m9Ya9Domi}MlQ;%EH`1`TM{4~Tj~nLDANdBbRC$qHmWK9tXN=KfLsJ)mAUXQ$th) zj#2qrg2|0G&zB}EXZVe#a<>y3;p0?i)L7rk{AZ;&J*po*Icd1<*gLT#P(ga$ASp6& zE)~d087xR_O1O;o$lXkIwb)dp-eBW{3yvLK#$)(nHE7Gx0VX@tYePHr)`FDP@N3wl6PD`$t0&MD5`}pIX>|-Qx z8Zg8JZ0mohq};glSQVaU#(U%bDm?&NI$GCHo?!4Kp$n9uXg9R3ds**U#FHT+_~hx3 zEr*yw8?88lNWi!6zdaLEv9==jlzyJBOKYWX~q@AuL7Sp3{LOjPd|UU(jX}p zV0ljqMYhoRsnxM1doTF>Cs_hase>`vf+aAN&TWy2+JKwcev&yEuGF5r=#C<;Eb6CN zNOQhQeC>nEbhaU1ak)#|n%kO*7l#$E^oBbj<#hCsGNH5G#c)4OvcpWI(&DN`l1Xzt zz9d^YI_C5H6Z&s&DbiZ-`}@qy6zP}Fs+&N2+PjSzbolKkVQmk2i*n|OnBU=J zz6ZZyj!Nbq(Fk}-V%f*}<%|enI)`XY+27I>O+&SM9o1PiF!t4CdyV4N%%^&;AZdGR zdDMy@6o9??Bkz>5Hq!pA;%$epo>@m3mdFTl4ToNW7YFDpw9dZ}e(eyz`mS^%(_OP> zuNVvs;p_uU^Kh(FkYh$#4?6kOASP6I)cIEozjcP%kYHT!T!GEhx@~_*gL&*4ygz+3 zBfBGT^Q%9bV^U7~myvy0o4`t;gX$SgZlz);4}^S+1$L-ZQ^shr8#i9&!-yjnkW<@N zM~?yCt8XsHV=EgxM!A@<)8O^l);O9K7mS3K?4-_a78{1$nM%Hp#|J^-@2v(c5~hgv zdQK*9XI?S2%37TpBH`g2GaH(wXf3ATUzr1M{GYe5#xMvY!8T%;entu4v}`=%vH$q4$b@ z03KblIwm)Kd=|>@X}U2H#8*;~0*iLHDLB>P;pp@|z(}bd)bX<={}#BpqC&;G_LS_d z&YcBr+6zlSs$x|nrHV~deX)E=Y$z>J0soBXrQHrF{^6229n%N|Q<(@Ww zADiX(zVnJ4itebZ@#l^GKT<8|R2~6tl~FN$D$Crah@2SW7);PVTwgr?`EyEp%bIHR zN;}uQpka4iuz4~>HFx6aj^xk2$v*U>IbvpRtNQqaHuJk`^xR$(V}WLM%c852&V4>6 z?;tmmvwpwOo#%`XwvAS!IKd+i)23#zmuJbXB0tS(Zlq+TR^i);|5y*+6%r{~iAmyk zSK|_fVP`>edaFjJ!$YB;?vH)qoq^*>Cf1mI$X*axq)~mU zqb&)QRl^wga5;Vd$JR7)Nct$Iu@~3qr_jA6tr9_ws0qgV{f^?{xRYNm@dyee-WaZf zJ~%L!f8n5r+>Wap_Y)>pO8)G-%%pPPL;3Za6Nil6*81w*!V38w`LFkKRQU9N)V*Xd za6W68c{te05YqBd%su3L?Wf$fCfO?q9C3+qccH}ra{XntsnBV_hjz^~3W7U4oJ*5b zrbGs&XAkEt@7hgFQQn)%XY#efwx|?++fmM|bvav^0m<7vg#yPe!}6|rBb*uwW-j=w z`E}?$q&pv>?^vRp6&3Z=6&3$>t%ANx$qIQQt=6e`BU;z6=miz7l zdiM$PO`JwLmW#p$3z^hCJ#SbNl3a3|Fj!{!_#mjTI~d7CMcI|9Irm!&2bsI+Zl8+n z=5z)fV<^1q6Do(&4Lj$ ztZsa=VBqVcXKe&W5K{;?!hoZ zWMzQ2>(0^TxQx@ z5>U81FT@^h2jlg3N1*S)001dje*^^T3PXbIV2;in(#*TfZOkBNdue70Uk7h$2-6S3GwIg@MgKD_>Dse<_-07Mj)Nx9-wPZh#lMqDb37` zZU_BNj&6l2`=cv=@8G)pi{2Y)&!>(q2%+Bx4d4?1gT;8j0z3lZe1Ek^w`yzuVeR4l zyNYN%`TQXWK7L*>pS%0tS$HFregEw5A6j@Dp|2bH3}D`HA1^3O*%#)4Wch2S2sa<^ zzxL_l4ZE)T<+q!?10UL{Up@cTMpa!~?+=@686BP75x*?1(SJqSL;t`be7xL#VeFxN zFgKVx+7WLwGymV=ki~of}*UK3_ z5+H7W_3E0+9!&*UsTs|kdW6? zmu42=1^-#2=LSJKz`fk1nYElfeEk1xFmiT>86qLqqVbFHgN1~JM8HA!`1afI<9JREH`3{XXIpAdY;$ic)<44E$f1 z44vS99{+bde?tFYk@rIS!M$8{y>#tdU{K^g=lMtAKbQ>AbDcNRD?t6fd8z*mC-qyp zs-bP+UIBmU-w@{cTj{q6$<6tfR3Ol=Nk9Su{Y`#vh%e0kR|wEL{?-I_f_ON>(6jCD zL+x+%&i^2dAy82P7z853V=n*`L8q~UJr6`g6viVaDgs6)xR?;c0rL0k-f#z`AH)kL z?}*L;bez#c>sOpX9KW)U^Y77qPB3(iaDn+Hz+mRz!V8k(yPi=0_Igs+Q;xQ_#9sxF zx}K6G)UPwo&Y*McfaXoVW2S2~3+y7t>XzKrT z@-ON8AG!V`*T1B|zXbkIcKt`Le@TIV3H+bz`u`>u!Jj8=msAQYP1Q0Lm?i6H*k&BxE*rCvNd z$)-O1sYbhaIK4OpnZf2Pc&pws>y4E)!xlN8NVlydXqJ5UJ%}Ms@?FHNj>N_+{CI=$ zy$(L#7yNfwI&CPe16=6+qv;*L+?kLXu0+jdZ1P9y1hL@{FhI<}@CP&iW;?)xf3xrc z^1>4jn2ud|BMAWd;24G-fx6UHz{6(}GwK2)41`z`(8ORmzg$1aT!7buXsVcRzG0^S zIyhSl8Y~6fB!AF@Q;02~MZs3B^i2ExQ+WHTnw+3=5{$GXolZr^8oMkbT#tx@z-B~_ zNN#g$FDBf>X|EpkV&Zue8(d@{M=1X_8|9@h@j{i{9fCU)?<9l5@GxGM-Jig=zXu4a zBRO6M7o#5L*KprI_hg`1+leXukpB`Wi%m-r2?sXuE1HsR4JcW;-k-W*i)XC4*U1F5Rr3v01bt1x*YzP!-1b|X zXZN-AFd={RXbZo&7*kvv=rBw5{`u|guGIAB3!isa`@(6$JM!3tPIwy-=W^sS6(3ci zbQQUCGE6s&s6L_u_ddj8nH}kIeRi;n_(Z>=tZWBJrh1g7=iNlZ=+4#NIwOL_Q5myN zb$*g2BlwUNRcH6(J#d^Jg4co1zz?Xu5uc$KQh`kcr&>56CuANP3OV^IWb0*4uW4Fm z*Fhap0- zft@u;RwwoudGs^#s4`kTBP<0qnkGyu(L@B&0W(5wB7Mr%@ks_4 zVu-Zu78dbx0^eY)RMX~{Nz@~0GcoYKn^igXFqmD*;xq)Czk9@R`=cjqn%T_`tN8b6 zX0uqP5O#w2C~*2x__|g5@LXwqYPir1wF$bIx?YnSE6==%=a0&;Zhp=Aq+rq9@Cr2U zPSM*s8mF3j01}mLCyW;I#k8ftl+h)hJ+D+3GfT%00pQAOV-XM{xla^GcP5`HB+Y3^-QM-J(h)5|d+@-s6wxJW8yWX`=SE2t%lSnnq; z=%z3Pb(XTfTKN!B#CkgC=jEU|?E@zx<=&7x?Q<4qmWlZR|~l22|@;WVtZg00UlaZDyqsL|MD695sF)dq?hAMO4<;!?rK^5teN|DMC37jId0GW-d0!VP@l=-`)DG z{ovDmQ0qlh%Ez~{jksXIh7a+zu8(q29nEyxV+_R6>sI{r2SsdsHe1RuoS{LIIZU=R zckPCVYJ@Dv_bkZqC&)I328YV>N-X5LfAp+j*u&J98;HiU@zc1O#;c<^I7pZTxkNbp zZW38_d^lX%Yir90?>rOCpUZPSTvgq_$jwr*hEUgsYM`0{+1n!Li>lS0c>;%rvV!S% zo}I%)pS?N&wSAP$E2v5h&v)Icx6lvcC!M_~naJ3u1kJ{Y2#-|5e(>SUdW9W3&za(; zpl!Y8jT@@`XFK4}jzE2>eB$U2GkUdM;V4wpO!|0Z0Kw=58)<}xXMf|>=#v9(hTa`j2YJmiR%Rw;|2LiAa+uzLfhn| z_#3`0#L)%t(w5J2LL^l0pF>S`>M_RA#>x67WNvGz4h z^%91qUSfn}y9_k!H`}DzefJCI$Q(-gdc#5kL{XQwdL*_rx1zAZD=BgQX)cU$50@p- z^k7C@GE((fJ#;iuWjwp1(ASzX;#sj}%YLs{-<1OUZ8L+E13$3`7&I+FwFtyVB3F|; zrP*xb?jQCqipHCmNY@6^RL&znWdvGjWIf4_7Y)Z4j9!Py%}{cZbco2|keyIBW>SUI zcmniH^Udiah2TJm#-MMV!dx7j>b4V~eS;UCNQij_Hnir_gipcN38Wn3-)snrk{CM_ zZw-gb6mYbE-QbrxHtkRI%cZ_`%hTgi7l25qJvZ8l3O0T+Wr_6VjA}|LbJ>*+!)Q;6 zy5a`Jn=cL(&6u@rViG*OXlYO-l52bXGp4=h@Pq$_EZgEZd2cx1j&p`m9TsAxhrd)R zG7iR{;QbAp9+Zm@GAa-S#Kb8*3)~>Ej|o6|$9I`J8gzp};poEd z@ho*bo?7XP^3F&$ILW3<_IQ17OuX<-?531lR(o^SoHJj^)}KcK_H3R*DKz2y)Zvqx z%;&>%TB<>+SAL<&7!Tupq-qKY{dhb&3J*l=)1Xm5ECHN26!^r%&%F>kN=(EN7i^dWMGyORSVg90kmHO#-3X@ zrD87R-9l$oRR|O#k#Xm&qeubJn@c<&EnH$duv`s@!)Pe-=f?VGlO6rNa&3C=+y>8f zu}jRPW&C0D?s4KcOML-3Wm&mK)kggiVzXhTxNW)SCe3wu^=rP*IbIrK=d%uZmZKH1D@=LjtMk6;ino1*YfNUU%1wh__Waa5fy;gA zrZ$nBz`YFbz2UaZ2*Hv*GR(A4Mhc~$N0 zq+vVqr+q)mD;e3*tF(csVM5sH?2SH~8{H4#*|FuI+Aw30uh5RO%woGki$?#`SYmYU zqFx51;qwDHOOBltPC4l3@T*xxux%KlLXEA*j4jJNAqD|K_h!!+AM&#RZO46TCI%-w zIvEU4B~LyzAY1V!Uf*6`-G8yFXCQ{HC|NCb{z<8&eu!8)-%IYNWV5={cx%U_PDb7P zsaZfUpui|kkkDdu)efa2x#>bE2D(`Oz6?dh`WjXAD(4BtW4%Xj%L=aEgm`=%f#q?5 z_V$0+txGs!gxtRHp<;w{shG;OTRe||o=s=rpxJa^_C)fyUY#HpXDk?;4pzNve-1A9 zf7o$PEawN@9bIJ_hc2|EaU1KTSyIa?{Q<*kVWSJCRmV{jH0Ts?AX1cH8Ax^wRW10@ zf?qu>V zUPfV*D`3+WQGa*5t(UlSUVQtzYFGqT^Ni^E_pMDs2P4y`h$Ncuuhj7wRUwKPcB206 zv8nBKkHpuVd=Pn;N~+~pQzFaLh%(0bxLN*38De@waxRj7Wi&mB)4@%XUkevMGWx^L zCcy;sr%`m;+M;cmURw`v)^w@7tI)JhA2`lmXe&QZ>f#ac&9loUvgee~G;smD&{nV?ePn*CpGzlN%O><@l>Tt(CVETyp{a$g_2W^~mB^i{!x_7E{6}>3 zSV{LbT@K$ID+XOT64tV1iJ1u$@4_|lvm}f#b+xbfcD64DW11#|JrG241Pth4=DP8< zu4=c0So5g?EXJrn6tw8yu(Lo2?~dNIthZh?!JNTlgfG2y0fM510=(b2x*pX37|0P> z=%Z($zYP|?6?<&1A5`qrmtf)ULr!9P5B|*+VJ?gXei6ucZ0yy$Qhq^2fR7e>U3WOI|q(I(m)3ZJA5-?%J|U{i-L%s*F=^g?9?1e6sm=lY~xG4QT8C z_|o`k(Ow|#?gNJPnAeiWAxbV)SAb&qOurUdvF(wZP&}xml&FGPU8Y%^^aF@e z1dUlwj*bRjO}o0DZM}hP91hXML|!y&3{Hw5Wrs@~wZyzr9}`0XF@xLOHrlH+)BhYNP6w7x7v2y% zilfu+R}Ax8TE31W4T|_F22Jq38L$q{-STRs{8JxhT?I4+XR8X*+cXVnl%tL# zaGNuF;Gb~aKhPWJYJk;+@a|v?vbBQr<^M|NkCT96&$`qObtBBX+t-VuVqH4 zCUB(HrWVL5RFuUeV6-k*tLhf^an%|#*&5>da-#{7a^}0+Z?%u7@Y1#NvfAwSyZ#&Q z5>xcMtzt!}6gU|bem(Boc0y9X8M3=|JyQ9RIBi$ibg@5R#a+kVs6}v&r4^~N@X10# z-L3Y%(?|5t`X5&Z7fz9PCnZ&Wa1T6nosTwcNt4Q+BCo_4tV%Zvy@d*GCf>vgjeLkH zb@)m|D`T>c^gc%3!ptd=)JX%bA0NTv@ngjC3%uLG=a@I54a0!pF;>r{6Q1$~J;@Y2 zm$XIipFclusS;N>2hjj%IsLR;y9MXmq689h3ts?GE#rxwhBHnuzyP5yC77hr3mxat zPatQaYkUdw`9-&_zdHgrsL&_;;0qXE)`Vi4qC_=uMerxwAd!+wB0y2XTglA5BTBciER0ny@#X|i5)i6 z8&#UY7e#?$S@I-8SaPKmS8>vm{jb@oo)e1z~}}oV`8bd(^2N$1yMhQ z9s3_GK3Tc_GV7-xAoVA`_`4EJ->L0f0`vjfCam%HM58pl88BQW-r1XW|7#fuNBzD< zrvgB?bueP%@Y)5Z)NF$FlY^Ao)8el4C76N^%KPgn)OlqRxTzRd+}yUznjKv;4G7e@13x&h!2m5Qq| z(w&5bH&nk|yXj>)WAup6yE&+N2Z1T!kE|IG}0t(>Z!Lwb(WPBz&7tm{n!4zJE z^06DmWCV%1p(RC?h8HirDUnW-*j_EcG%W7k8*!A<6V|!71<+_SjA#K^n+@Z+u6{4U z9My^&aix8di7J)FlG_Mi1BHtdhM4_y8xta0o;pn*glwjfqe>W%n zT-+slRs9L(DRn7KD#X))VF%!tX(VW>a?!>T;Gsyr8E5QDyKr}XxjjLiHrCcHhoH}D zxpRrj3sltJqHL{m(X&~Hu$(+~sp2ntH$s8|63y)m5y#4V<9D~6A$JTQhW)P%^M7Y* m_y;!spz7cCgZRxARvKRm*|I@`Hu~xmpsuW=^jg6-@_zxLE}!uL literal 0 HcmV?d00001 diff --git a/switch/res/icon.jpg b/switch/res/icon.jpg deleted file mode 100644 index cb515da077efce82a1d9aab5241a7cf15d30fd57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25090 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 G-TwjAB&N^+ literal 0 HcmV?d00001 diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 2d0b71f..c23f470 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -281,7 +281,7 @@ bool MainApplication::Load() // Create a view this->rootFrame = new brls::TabFrame(); this->rootFrame->setTitle("Chiaki: Open Source PlayStation Remote Play Client"); - this->rootFrame->setIcon(BOREALIS_ASSET("icon.jpg")); + this->rootFrame->setIcon(BOREALIS_ASSET("icon.png")); brls::List *config = new brls::List(); brls::List *add_host = new brls::List(); From 2a4b67b58e4c2d9ec01f10db7fcc0a681c1ff6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 11 Jan 2021 20:13:43 +0100 Subject: [PATCH 039/104] Complete ControllerState in JNI --- android/app/src/main/cpp/chiaki-jni.c | 58 ++++++++++ .../java/com/metallic/chiaki/lib/Chiaki.kt | 103 ++++++++++++++++-- 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index 49229f5..72b6af4 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -140,6 +140,20 @@ typedef struct android_chiaki_session_t jfieldID java_controller_state_left_y; jfieldID java_controller_state_right_x; jfieldID java_controller_state_right_y; + jfieldID java_controller_state_touches; + jfieldID java_controller_state_gyro_x; + jfieldID java_controller_state_gyro_y; + jfieldID java_controller_state_gyro_z; + jfieldID java_controller_state_accel_x; + jfieldID java_controller_state_accel_y; + jfieldID java_controller_state_accel_z; + jfieldID java_controller_state_orient_x; + jfieldID java_controller_state_orient_y; + jfieldID java_controller_state_orient_z; + jfieldID java_controller_state_orient_w; + jfieldID java_controller_touch_x; + jfieldID java_controller_touch_y; + jfieldID java_controller_touch_id; AndroidChiakiVideoDecoder video_decoder; AndroidChiakiAudioDecoder audio_decoder; @@ -305,6 +319,22 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject session->java_controller_state_left_y = E->GetFieldID(env, controller_state_class, "leftY", "S"); session->java_controller_state_right_x = E->GetFieldID(env, controller_state_class, "rightX", "S"); session->java_controller_state_right_y = E->GetFieldID(env, controller_state_class, "rightY", "S"); + session->java_controller_state_touches = E->GetFieldID(env, controller_state_class, "touches", "[L"BASE_PACKAGE"/ControllerTouch;"); + session->java_controller_state_gyro_x = E->GetFieldID(env, controller_state_class, "gyroX", "F"); + session->java_controller_state_gyro_y = E->GetFieldID(env, controller_state_class, "gyroY", "F"); + session->java_controller_state_gyro_z = E->GetFieldID(env, controller_state_class, "gyroZ", "F"); + session->java_controller_state_accel_x = E->GetFieldID(env, controller_state_class, "accelX", "F"); + session->java_controller_state_accel_y = E->GetFieldID(env, controller_state_class, "accelY", "F"); + session->java_controller_state_accel_z = E->GetFieldID(env, controller_state_class, "accelZ", "F"); + session->java_controller_state_orient_x = E->GetFieldID(env, controller_state_class, "orientX", "F"); + session->java_controller_state_orient_y = E->GetFieldID(env, controller_state_class, "orientY", "F"); + session->java_controller_state_orient_z = E->GetFieldID(env, controller_state_class, "orientZ", "F"); + session->java_controller_state_orient_w = E->GetFieldID(env, controller_state_class, "orientW", "F"); + + jclass controller_touch_class = E->FindClass(env, BASE_PACKAGE"/ControllerTouch"); + session->java_controller_touch_x = E->GetFieldID(env, controller_touch_class, "x", "S"); + session->java_controller_touch_y = E->GetFieldID(env, controller_touch_class, "y", "S"); + session->java_controller_touch_id = E->GetFieldID(env, controller_touch_class, "id", "B"); chiaki_session_set_event_cb(&session->session, android_chiaki_event_cb, session); chiaki_session_set_video_sample_cb(&session->session, android_chiaki_video_decoder_video_sample, &session->video_decoder); @@ -382,6 +412,34 @@ JNIEXPORT void JNICALL JNI_FCN(sessionSetControllerState)(JNIEnv *env, jobject o controller_state.left_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_left_y); controller_state.right_x = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_x); controller_state.right_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_y); + jobjectArray touch_array = E->GetObjectField(env, controller_state_java, session->java_controller_state_touches); + size_t touch_array_len = (size_t)E->GetArrayLength(env, touch_array); + for(size_t i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + { + if(i < touch_array_len) + { + jobject touch = E->GetObjectArrayElement(env, touch_array, i); + controller_state.touches[i].x = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_x); + controller_state.touches[i].y = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_y); + controller_state.touches[i].id = (int8_t)E->GetByteField(env, touch, session->java_controller_touch_id); + } + else + { + controller_state.touches[i].x = 0; + controller_state.touches[i].y = 0; + controller_state.touches[i].id = -1; + } + } + controller_state.gyro_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_x); + controller_state.gyro_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_y); + controller_state.gyro_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_z); + controller_state.accel_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_x); + controller_state.accel_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_y); + controller_state.accel_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_z); + controller_state.orient_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_x); + controller_state.orient_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_y); + controller_state.orient_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_z); + controller_state.orient_w = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_w); chiaki_session_set_controller_state(&session->session, &controller_state); } diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index d638cb2..0700a7c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -150,6 +150,14 @@ class ChiakiLog(val levelMask: Int, val callback: (level: Int, text: String) -> private fun maxAbs(a: Short, b: Short) = if(abs(a.toInt()) > abs(b.toInt())) a else b +private val CONTROLLER_TOUCHES_MAX = 2 // must be the same as CHIAKI_CONTROLLER_TOUCHES_MAX + +data class ControllerTouch( + val x: UShort = 0U, + val y: UShort = 0U, + val id: Byte = -1 // -1 = up +) + data class ControllerState constructor( var buttons: UInt = 0U, var l2State: UByte = 0U, @@ -157,25 +165,37 @@ data class ControllerState constructor( var leftX: Short = 0, var leftY: Short = 0, var rightX: Short = 0, - var rightY: Short = 0 + var rightY: Short = 0, + private var touchIdNext: UByte = 0U, + var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), + var gyroX: Float = 0.0f, + var gyroY: Float = 0.0f, + var gyroZ: Float = 0.0f, + var accelX: Float = 0.0f, + var accelY: Float = 1.0f, + var accelZ: Float = 0.0f, + var orientX: Float = 0.0f, + var orientY: Float = 0.0f, + var orientZ: Float = 0.0f, + var orientW: Float = 1.0f ){ companion object { val BUTTON_CROSS = (1 shl 0).toUInt() val BUTTON_MOON = (1 shl 1).toUInt() - val BUTTON_BOX = (1 shl 2).toUInt() - val BUTTON_PYRAMID = (1 shl 3).toUInt() + val BUTTON_BOX = (1 shl 2).toUInt() + val BUTTON_PYRAMID = (1 shl 3).toUInt() val BUTTON_DPAD_LEFT = (1 shl 4).toUInt() val BUTTON_DPAD_RIGHT = (1 shl 5).toUInt() - val BUTTON_DPAD_UP = (1 shl 6).toUInt() + val BUTTON_DPAD_UP = (1 shl 6).toUInt() val BUTTON_DPAD_DOWN = (1 shl 7).toUInt() - val BUTTON_L1 = (1 shl 8).toUInt() - val BUTTON_R1 = (1 shl 9).toUInt() + val BUTTON_L1 = (1 shl 8).toUInt() + val BUTTON_R1 = (1 shl 9).toUInt() val BUTTON_L3 = (1 shl 10).toUInt() val BUTTON_R3 = (1 shl 11).toUInt() - val BUTTON_OPTIONS = (1 shl 12).toUInt() + val BUTTON_OPTIONS = (1 shl 12).toUInt() val BUTTON_SHARE = (1 shl 13).toUInt() - val BUTTON_TOUCHPAD = (1 shl 14).toUInt() + val BUTTON_TOUCHPAD = (1 shl 14).toUInt() val BUTTON_PS = (1 shl 15).toUInt() } @@ -186,8 +206,73 @@ data class ControllerState constructor( leftX = maxAbs(leftX, o.leftX), leftY = maxAbs(leftY, o.leftY), rightX = maxAbs(rightX, o.rightX), - rightY = maxAbs(rightY, o.rightY) + rightY = maxAbs(rightY, o.rightY), + touches = touches.zip(o.touches) { a, b -> if(a.id >= 0) a else b }.toTypedArray(), + gyroX = gyroX, + gyroY = gyroY, + gyroZ = gyroZ, + accelX = accelX, + accelY = accelY, + accelZ = accelZ, + orientX = orientX, + orientY = orientY, + orientZ = orientZ, + orientW = orientW ) + + override fun equals(other: Any?): Boolean + { + if(this === other) return true + if(javaClass != other?.javaClass) return false + + other as ControllerState + + if(buttons != other.buttons) return false + if(l2State != other.l2State) return false + if(r2State != other.r2State) return false + if(leftX != other.leftX) return false + if(leftY != other.leftY) return false + if(rightX != other.rightX) return false + if(rightY != other.rightY) return false + if(touchIdNext != other.touchIdNext) return false + if(!touches.contentEquals(other.touches)) return false + if(gyroX != other.gyroX) return false + if(gyroY != other.gyroY) return false + if(gyroZ != other.gyroZ) return false + if(accelX != other.accelX) return false + if(accelY != other.accelY) return false + if(accelZ != other.accelZ) return false + if(orientX != other.orientX) return false + if(orientY != other.orientY) return false + if(orientZ != other.orientZ) return false + if(orientW != other.orientW) return false + + return true + } + + override fun hashCode(): Int + { + var result = buttons.hashCode() + result = 31 * result + l2State.hashCode() + result = 31 * result + r2State.hashCode() + result = 31 * result + leftX + result = 31 * result + leftY + result = 31 * result + rightX + result = 31 * result + rightY + result = 31 * result + touchIdNext.hashCode() + result = 31 * result + touches.contentHashCode() + result = 31 * result + gyroX.hashCode() + result = 31 * result + gyroY.hashCode() + result = 31 * result + gyroZ.hashCode() + result = 31 * result + accelX.hashCode() + result = 31 * result + accelY.hashCode() + result = 31 * result + accelZ.hashCode() + result = 31 * result + orientX.hashCode() + result = 31 * result + orientY.hashCode() + result = 31 * result + orientZ.hashCode() + result = 31 * result + orientW.hashCode() + return result + } } class QuitReason(val value: Int) From 2906cfdd69ff0343c10bada4362387cc0627424a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 21:08:33 +0100 Subject: [PATCH 040/104] Add Motion to Android --- .../metallic/chiaki/session/StreamInput.kt | 89 +++++++++++++++++-- .../metallic/chiaki/stream/StreamActivity.kt | 6 +- .../metallic/chiaki/stream/StreamViewModel.kt | 13 +-- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt index e96b7b5..2518ce1 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt @@ -1,19 +1,36 @@ package com.metallic.chiaki.session -import android.util.Log -import android.view.InputDevice -import android.view.KeyEvent -import android.view.MotionEvent +import android.content.Context +import android.hardware.* +import android.view.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.lib.ControllerState -class StreamInput(val preferences: Preferences) +class StreamInput(val context: Context, val preferences: Preferences) { var controllerStateChangedCallback: ((ControllerState) -> Unit)? = null val controllerState: ControllerState get() { - val controllerState = keyControllerState or motionControllerState + val controllerState = sensorControllerState or keyControllerState or motionControllerState + + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + when(windowManager.defaultDisplay.rotation) + { + Surface.ROTATION_90 -> { + controllerState.accelX *= -1.0f + controllerState.accelZ *= -1.0f + controllerState.gyroX *= -1.0f + controllerState.gyroZ *= -1.0f + controllerState.orientX *= -1.0f + controllerState.orientZ *= -1.0f + } + else -> {} + } // prioritize motion controller's l2 and r2 over key // (some controllers send only key, others both but key earlier than full press) @@ -25,6 +42,7 @@ class StreamInput(val preferences: Preferences) return controllerState or touchControllerState } + private val sensorControllerState = ControllerState() // from Motion Sensors private val keyControllerState = ControllerState() // from KeyEvents private val motionControllerState = ControllerState() // from MotionEvents var touchControllerState = ControllerState() @@ -36,6 +54,65 @@ class StreamInput(val preferences: Preferences) private val swapCrossMoon = preferences.swapCrossMoon + private val sensorEventListener = object: SensorEventListener { + override fun onSensorChanged(event: SensorEvent) + { + when(event.sensor.type) + { + Sensor.TYPE_ACCELEROMETER -> { + sensorControllerState.accelX = event.values[1] / SensorManager.GRAVITY_EARTH + sensorControllerState.accelY = event.values[2] / SensorManager.GRAVITY_EARTH + sensorControllerState.accelZ = event.values[0] / SensorManager.GRAVITY_EARTH + } + Sensor.TYPE_GYROSCOPE -> { + sensorControllerState.gyroX = event.values[1] + sensorControllerState.gyroY = event.values[2] + sensorControllerState.gyroZ = event.values[0] + } + Sensor.TYPE_ROTATION_VECTOR -> { + val q = floatArrayOf(0f, 0f, 0f, 0f) + SensorManager.getQuaternionFromVector(q, event.values) + sensorControllerState.orientX = q[2] + sensorControllerState.orientY = q[3] + sensorControllerState.orientZ = q[1] + sensorControllerState.orientW = q[0] + } + else -> return + } + controllerStateUpdated() + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} + } + + private val lifecycleObserver = object: LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() + { + val samplingPeriodUs = 4000 + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + listOfNotNull( + sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), + sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), + sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + ).forEach { + sensorManager.registerListener(sensorEventListener, it, samplingPeriodUs) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun onPause() + { + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + sensorManager.unregisterListener(sensorEventListener) + } + } + + fun observe(lifecycleOwner: LifecycleOwner) + { + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + } + private fun controllerStateUpdated() { controllerStateChangedCallback?.let { it(controllerState) } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 67d425b..903b408 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -5,6 +5,7 @@ package com.metallic.chiaki.stream import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.AlertDialog +import android.content.res.Configuration import android.graphics.Matrix import android.os.Bundle import android.os.Handler @@ -58,9 +59,11 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } viewModel = ViewModelProvider(this, viewModelFactory { - StreamViewModel(Preferences(this), LogManager(this), connectInfo) + StreamViewModel(application, connectInfo) })[StreamViewModel::class.java] + viewModel.input.observe(this) + setContentView(R.layout.activity_stream) window.decorView.setOnSystemUiVisibilityChangeListener(this) @@ -305,7 +308,6 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event) - } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt index ebaaf80..df69378 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt @@ -2,19 +2,22 @@ package com.metallic.chiaki.stream -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import android.app.Application +import android.content.Context +import androidx.lifecycle.* import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.session.StreamSession import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.lib.* import com.metallic.chiaki.session.StreamInput -class StreamViewModel(val preferences: Preferences, val logManager: LogManager, val connectInfo: ConnectInfo): ViewModel() +class StreamViewModel(val application: Application, val connectInfo: ConnectInfo): ViewModel() { + val preferences = Preferences(application) + val logManager = LogManager(application) + private var _session: StreamSession? = null - val input = StreamInput(preferences) + val input = StreamInput(application, preferences) val session = StreamSession(connectInfo, logManager, preferences.logVerbose, input) private var _onScreenControlsEnabled = MutableLiveData(preferences.onScreenControlsEnabled) From 3b85e147b6056640f17178a1336e1c9f0be6c755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 15:00:53 +0100 Subject: [PATCH 041/104] Add Rumble to Android --- README.md | 12 ++---------- android/app/src/main/AndroidManifest.xml | 1 + android/app/src/main/cpp/chiaki-jni.c | 10 ++++++++++ .../com/metallic/chiaki/common/Preferences.kt | 7 ++++++- .../main/java/com/metallic/chiaki/lib/Chiaki.kt | 6 ++++++ .../com/metallic/chiaki/session/StreamSession.kt | 3 +++ .../metallic/chiaki/settings/SettingsFragment.kt | 2 ++ .../com/metallic/chiaki/stream/StreamActivity.kt | 16 ++++++++++++++++ android/app/src/main/res/drawable/ic_rumble.xml | 9 +++++++++ android/app/src/main/res/values/strings.xml | 5 ++++- android/app/src/main/res/xml/preferences.xml | 6 ++++++ 11 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_rumble.xml diff --git a/README.md b/README.md index b84b73d..5129f3a 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,9 @@ for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potent ![Screenshot](assets/screenshot.png) -## Features - -Everything necessary for a full streaming session, including the initial -registration and wakeup of the console, is supported. -The following features however are yet to be implemented: -* Rumble -* Accelerometer/Gyroscope - ## Installing -You can either download a pre-built release (easier) or build Chiaki from source. +You can either download a pre-built release or build Chiaki from source. ### Downloading a Release @@ -48,7 +40,7 @@ make ``` For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md). -in + ## Usage If your Console is on your local network, is turned on or in standby mode and does not have Discovery explicitly disabled, Chiaki should find it. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 14b0466..fa14662 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + CallVoidMethod(env, session->java_session, + session->java_session_event_rumble_meth, + (jint)event->rumble.left, + (jint)event->rumble.right); + break; + default: + break; } (*global_vm)->DetachCurrentThread(global_vm); @@ -310,6 +319,7 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject session->java_session_event_connected_meth = E->GetMethodID(env, session->java_session_class, "eventConnected", "()V"); session->java_session_event_login_pin_request_meth = E->GetMethodID(env, session->java_session_class, "eventLoginPinRequest", "(Z)V"); session->java_session_event_quit_meth = E->GetMethodID(env, session->java_session_class, "eventQuit", "(ILjava/lang/String;)V"); + session->java_session_event_rumble_meth = E->GetMethodID(env, session->java_session_class, "eventRumble", "(II)V"); jclass controller_state_class = E->FindClass(env, BASE_PACKAGE"/ControllerState"); session->java_controller_state_buttons = E->GetFieldID(env, controller_state_class, "buttons", "I"); diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index c9691b5..d779c5a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -68,11 +68,16 @@ class Preferences(context: Context) get() = sharedPreferences.getBoolean(onScreenControlsEnabledKey, true) set(value) { sharedPreferences.edit().putBoolean(onScreenControlsEnabledKey, value).apply() } - val touchpadOnlyEnabledKey get() = resources.getString(R.string.preferences_touchpad_only_key) + val touchpadOnlyEnabledKey get() = resources.getString(R.string.preferences_touchpad_only_enabled_key) var touchpadOnlyEnabled get() = sharedPreferences.getBoolean(touchpadOnlyEnabledKey, false) set(value) { sharedPreferences.edit().putBoolean(touchpadOnlyEnabledKey, value).apply() } + val rumbleEnabledKey get() = resources.getString(R.string.preferences_rumble_enabled_key) + var rumbleEnabled + get() = sharedPreferences.getBoolean(rumbleEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(rumbleEnabledKey, value).apply() } + val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key) var logVerbose get() = sharedPreferences.getBoolean(logVerboseKey, false) diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 0700a7c..023da90 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -289,6 +289,7 @@ sealed class Event object ConnectedEvent: Event() data class LoginPinRequestEvent(val pinIncorrect: Boolean): Event() data class QuitEvent(val reason: QuitReason, val reasonString: String?): Event() +data class RumbleEvent(val left: UByte, val right: UByte): Event() class CreateError(val errorCode: ErrorCode): Exception("Failed to create a native object: $errorCode") @@ -344,6 +345,11 @@ class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean) event(QuitEvent(QuitReason(reasonValue), reasonString)) } + private fun eventRumble(left: Int, right: Int) + { + event(RumbleEvent(left.toUByte(), right.toUByte())) + } + fun setSurface(surface: Surface) { ChiakiNative.sessionSetSurface(nativePtr, surface) diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index 059840c..d7f127f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -25,6 +25,8 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va private val _state = MutableLiveData(StreamStateIdle) val state: LiveData get() = _state + private val _rumbleState = MutableLiveData(RumbleEvent(0U, 0U)) + val rumbleState: LiveData get() = _rumbleState var surfaceTexture: SurfaceTexture? = null @@ -86,6 +88,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va event.pinIncorrect ) ) + is RumbleEvent -> _rumbleState.postValue(event) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index f38f619..7a4506e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -26,6 +26,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() { preferences.logVerboseKey -> preferences.logVerbose preferences.swapCrossMoonKey -> preferences.swapCrossMoon + preferences.rumbleEnabledKey -> preferences.rumbleEnabled else -> defValue } @@ -35,6 +36,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() { preferences.logVerboseKey -> preferences.logVerbose = value preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value + preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value } } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 903b408..d94fa0d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -9,6 +9,8 @@ import android.content.res.Configuration import android.graphics.Matrix import android.os.Bundle import android.os.Handler +import android.os.VibrationEffect +import android.os.Vibrator import android.view.KeyEvent import android.view.MotionEvent import android.view.TextureView @@ -30,6 +32,7 @@ import com.metallic.chiaki.session.* import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment import com.metallic.chiaki.touchcontrols.TouchControlsFragment import kotlinx.android.synthetic.main.activity_stream.* +import kotlin.math.min private sealed class DialogContents private object StreamQuitDialog: DialogContents() @@ -105,6 +108,19 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> adjustTextureViewAspect() } + + if(Preferences(this).rumbleEnabled) + { + val vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator + viewModel.session.rumbleState.observe(this, Observer { + val amplitude = min(255, (it.left.toInt() + it.right.toInt()) / 2) + vibrator.cancel() + if(amplitude == 0) + return@Observer + if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) + vibrator.vibrate(VibrationEffect.createOneShot(1000, amplitude)) + }) + } } override fun onAttachFragment(fragment: Fragment) diff --git a/android/app/src/main/res/drawable/ic_rumble.xml b/android/app/src/main/res/drawable/ic_rumble.xml new file mode 100644 index 0000000..4ecdb61 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_rumble.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0f54522..0f7161d 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -88,6 +88,8 @@ H265 (PS5 only) Swap Cross/Moon and Box/Pyramid Buttons Swap face buttons if default mapping is incorrect (e.g. for 8BitDo controllers) + Rumble + Use phone vibration motor for rumble Are you sure you want to delete the registered console %s with ID %s? Are you sure you want to delete the console entry for %s? Keep @@ -101,7 +103,8 @@ discovery_enabled on_screen_controls_enabled - touchpad_only_enabled + touchpad_only_enabled + rumble_enabled log_verbose import_settings export_settings diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index 0416f6e..e87afdb 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -19,6 +19,12 @@ app:summary="@string/preferences_swap_cross_moon_summary" app:icon="@drawable/ic_gamepad" /> + + Date: Wed, 13 Jan 2021 15:23:03 +0100 Subject: [PATCH 042/104] Add Rumble Fallback for Pre-O Android --- .../java/com/metallic/chiaki/stream/StreamActivity.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index d94fa0d..a25772d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -5,12 +5,8 @@ package com.metallic.chiaki.stream import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.AlertDialog -import android.content.res.Configuration import android.graphics.Matrix -import android.os.Bundle -import android.os.Handler -import android.os.VibrationEffect -import android.os.Vibrator +import android.os.* import android.view.KeyEvent import android.view.MotionEvent import android.view.TextureView @@ -23,7 +19,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.* import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.metallic.chiaki.R -import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.lib.ConnectInfo @@ -117,8 +112,10 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe vibrator.cancel() if(amplitude == 0) return@Observer - if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) vibrator.vibrate(VibrationEffect.createOneShot(1000, amplitude)) + else + vibrator.vibrate(1000) }) } } From c1a4504470d1f0b0481364629edbffb6e11df01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 17:14:07 +0100 Subject: [PATCH 043/104] Add L3 and R3 to Android Touch Controls --- .../touchcontrols/TouchControlsFragment.kt | 2 + .../main/res/drawable/control_button_l3.xml | 12 +++++ .../drawable/control_button_l3_pressed.xml | 12 +++++ .../main/res/drawable/control_button_r3.xml | 12 +++++ .../drawable/control_button_r3_pressed.xml | 12 +++++ .../src/main/res/layout/fragment_controls.xml | 21 +++++++++ assets/controls/l3.svg | 36 +++++++++++++++ assets/controls/l3_raw.svg | 44 +++++++++++++++++++ assets/controls/r3.svg | 36 +++++++++++++++ assets/controls/r3_raw.svg | 44 +++++++++++++++++++ 10 files changed, 231 insertions(+) create mode 100644 android/app/src/main/res/drawable/control_button_l3.xml create mode 100644 android/app/src/main/res/drawable/control_button_l3_pressed.xml create mode 100644 android/app/src/main/res/drawable/control_button_r3.xml create mode 100644 android/app/src/main/res/drawable/control_button_r3_pressed.xml create mode 100644 assets/controls/l3.svg create mode 100644 assets/controls/l3_raw.svg create mode 100644 assets/controls/r3.svg create mode 100644 assets/controls/r3_raw.svg diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index 9c4ca51..644f5f3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -41,6 +41,8 @@ class TouchControlsFragment : Fragment() boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) + l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3) + r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3) optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) diff --git a/android/app/src/main/res/drawable/control_button_l3.xml b/android/app/src/main/res/drawable/control_button_l3.xml new file mode 100644 index 0000000..34c3382 --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_l3.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_l3_pressed.xml b/android/app/src/main/res/drawable/control_button_l3_pressed.xml new file mode 100644 index 0000000..43f239a --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_l3_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_r3.xml b/android/app/src/main/res/drawable/control_button_r3.xml new file mode 100644 index 0000000..0f2bcfd --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_r3.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_r3_pressed.xml b/android/app/src/main/res/drawable/control_button_r3_pressed.xml new file mode 100644 index 0000000..08635e8 --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_r3_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index a2c24df..0bf8011 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -112,6 +112,27 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/controls/l3_raw.svg b/assets/controls/l3_raw.svg new file mode 100644 index 0000000..f04135f --- /dev/null +++ b/assets/controls/l3_raw.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + L3 + + diff --git a/assets/controls/r3.svg b/assets/controls/r3.svg new file mode 100644 index 0000000..b5f1fa7 --- /dev/null +++ b/assets/controls/r3.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/controls/r3_raw.svg b/assets/controls/r3_raw.svg new file mode 100644 index 0000000..5f47c8e --- /dev/null +++ b/assets/controls/r3_raw.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + R3 + + From b69bf280f8656df483c7f2680d52168f5cdc8c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 19:33:54 +0100 Subject: [PATCH 044/104] Extend Face Button Touch Areas on Android --- .../chiaki/touchcontrols/ButtonView.kt | 27 ++++++++- .../metallic/chiaki/touchcontrols/Vector.kt | 7 +++ .../src/main/res/layout/fragment_controls.xml | 56 ++++++++++--------- android/app/src/main/res/values/dimens.xml | 6 +- 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index b2eba6b..e82ffac 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -8,6 +8,8 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.MotionEvent import android.view.View +import android.view.ViewGroup +import androidx.core.view.children import com.metallic.chiaki.R class ButtonView @JvmOverloads constructor( @@ -46,15 +48,36 @@ class ButtonView @JvmOverloads constructor( { super.onDraw(canvas) val drawable = if(buttonPressed) drawablePressed else drawableIdle - drawable?.setBounds(0, 0, width, height) + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) drawable?.draw(canvas) } + /** + * If this button overlaps with others in the same layout, + * let the one whose center is closest to the touch handle it. + */ + private fun bestFittingTouchView(x: Float, y: Float): View + { + val loc = locationOnScreen + Vector(x, y) + return (parent as? ViewGroup)?.children?.filter { + it is ButtonView + }?.filter { + val pos = it.locationOnScreen + loc.x >= pos.x && loc.x < pos.x + it.width && loc.y >= pos.y && loc.y < pos.y + it.height + }?.sortedBy { + (loc - (it.locationOnScreen + Vector(it.width.toFloat(), it.height.toFloat()) * 0.5f)).lengthSq + }?.firstOrNull() ?: this + } + override fun onTouchEvent(event: MotionEvent): Boolean { when(event.action) { - MotionEvent.ACTION_DOWN -> buttonPressed = true + MotionEvent.ACTION_DOWN -> { + if(bestFittingTouchView(event.x, event.y) != this) + return false + buttonPressed = true + } MotionEvent.ACTION_UP -> buttonPressed = false } return true diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt index 36216e8..208c41a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt @@ -2,6 +2,7 @@ package com.metallic.chiaki.touchcontrols +import android.view.View import kotlin.math.sqrt data class Vector(val x: Float, val y: Float) @@ -18,4 +19,10 @@ data class Vector(val x: Float, val y: Float) val lengthSq get() = x*x + y*y val length get() = sqrt(lengthSq) val normalized get() = this / length +} + +val View.locationOnScreen: Vector get() { + val v = intArrayOf(0, 0) + this.getLocationOnScreen(v) + return Vector(v[0].toFloat(), v[1].toFloat()) } \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 0bf8011..3082f35 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -1,10 +1,12 @@ + android:clipChildren="false" + tools:ignore="RtlHardcoded,RtlSymmetry"> + app:layout_constraintBottom_toBottomOf="parent" /> @@ -116,7 +130,6 @@ android:id="@+id/l3ButtonView" android:layout_width="64dp" android:layout_height="64dp" - android:padding="8dp" app:drawableIdle="@drawable/control_button_l3" app:drawablePressed="@drawable/control_button_l3_pressed" app:layout_constraintLeft_toLeftOf="parent" @@ -126,7 +139,6 @@ android:id="@+id/r3ButtonView" android:layout_width="64dp" android:layout_height="64dp" - android:padding="8dp" app:drawableIdle="@drawable/control_button_r3" app:drawablePressed="@drawable/control_button_r3_pressed" app:layout_constraintRight_toRightOf="parent" @@ -137,7 +149,6 @@ android:id="@+id/psButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginBottom="8dp" app:drawableIdle="@drawable/control_button_home" app:drawablePressed="@drawable/control_button_home_pressed" @@ -150,7 +161,6 @@ android:id="@+id/touchpadButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" app:drawableIdle="@drawable/control_button_touchpad" app:drawablePressed="@drawable/control_button_touchpad_pressed" @@ -162,7 +172,6 @@ android:id="@+id/l2ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginLeft="8dp" app:drawableIdle="@drawable/control_button_l2" @@ -174,7 +183,6 @@ android:id="@+id/l1ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginLeft="8dp" app:drawableIdle="@drawable/control_button_l1" @@ -186,7 +194,6 @@ android:id="@+id/shareButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginLeft="8dp" app:drawableIdle="@drawable/control_button_share" @@ -198,7 +205,6 @@ android:id="@+id/r2ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginRight="8dp" app:drawableIdle="@drawable/control_button_r2" @@ -210,7 +216,6 @@ android:id="@+id/r1ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginRight="8dp" app:drawableIdle="@drawable/control_button_r1" @@ -222,7 +227,6 @@ android:id="@+id/optionsButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginRight="8dp" app:drawableIdle="@drawable/control_button_options" diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml index 543f0f4..9ee2e9b 100644 --- a/android/app/src/main/res/values/dimens.xml +++ b/android/app/src/main/res/values/dimens.xml @@ -1,6 +1,10 @@ - 48dp + 88dp + 176dp + 24dp + 16dp + 64dp 48dp 32dp 48dp From 3a90ef0a658eac2506a17d87b55e084337814765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 20:24:25 +0100 Subject: [PATCH 045/104] Extend DPad Touch Area on Android --- .../java/com/metallic/chiaki/touchcontrols/DPadView.kt | 2 +- android/app/src/main/res/layout/fragment_controls.xml | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index 9386e8f..0006546 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -71,7 +71,7 @@ class DPadView @JvmOverloads constructor( else drawable = dpadIdleDrawable - drawable?.setBounds(0, 0, width, height) + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) //drawable?.alpha = 127 drawable?.draw(canvas) } diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 3082f35..7d4842f 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -49,10 +49,11 @@ From 510064c8996d1e2bdc69053bc85dde1b999989ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 14 Jan 2021 18:56:42 +0100 Subject: [PATCH 046/104] Use SurfaceView on Android --- android/app/src/main/cpp/video-decoder.c | 55 +++++---- .../java/com/metallic/chiaki/lib/Chiaki.kt | 4 +- .../metallic/chiaki/session/StreamSession.kt | 30 ++++- .../chiaki/stream/AspectRatioFrameLayout.kt | 68 +++++++++++ .../metallic/chiaki/stream/StreamActivity.kt | 110 ++++++++++-------- .../src/main/res/layout/activity_stream.xml | 19 ++- 6 files changed, 206 insertions(+), 80 deletions(-) create mode 100644 android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt diff --git a/android/app/src/main/cpp/video-decoder.c b/android/app/src/main/cpp/video-decoder.c index d57623d..1146ad8 100644 --- a/android/app/src/main/cpp/video-decoder.c +++ b/android/app/src/main/cpp/video-decoder.c @@ -26,29 +26,34 @@ ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *dec return chiaki_mutex_init(&decoder->codec_mutex, false); } +static void kill_decoder(AndroidChiakiVideoDecoder *decoder) +{ + chiaki_mutex_lock(&decoder->codec_mutex); + decoder->shutdown_output = true; + ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, 1000); + if(codec_buf_index >= 0) + { + CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); + AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); + AMediaCodec_stop(decoder->codec); + chiaki_mutex_unlock(&decoder->codec_mutex); + chiaki_thread_join(&decoder->output_thread, NULL); + } + else + { + CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!"); + AMediaCodec_stop(decoder->codec); + chiaki_mutex_unlock(&decoder->codec_mutex); + } + AMediaCodec_delete(decoder->codec); + decoder->codec = NULL; + decoder->shutdown_output = false; +} + void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder) { if(decoder->codec) - { - chiaki_mutex_lock(&decoder->codec_mutex); - decoder->shutdown_output = true; - ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, -1); - if(codec_buf_index >= 0) - { - CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); - AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); - AMediaCodec_stop(decoder->codec); - chiaki_mutex_unlock(&decoder->codec_mutex); - chiaki_thread_join(&decoder->output_thread, NULL); - } - else - { - CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!"); - AMediaCodec_stop(decoder->codec); - chiaki_mutex_unlock(&decoder->codec_mutex); - } - AMediaCodec_delete(decoder->codec); - } + kill_decoder(decoder); chiaki_mutex_fini(&decoder->codec_mutex); } @@ -56,6 +61,16 @@ void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder { chiaki_mutex_lock(&decoder->codec_mutex); + if(!surface) + { + if(decoder->codec) + { + kill_decoder(decoder); + CHIAKI_LOGI(decoder->log, "Decoder shut down after surface was removed"); + } + return; + } + if(decoder->codec) { #if __ANDROID_API__ >= 23 diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 023da90..b8d7204 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -91,7 +91,7 @@ private class ChiakiNative @JvmStatic external fun sessionStart(ptr: Long): Int @JvmStatic external fun sessionStop(ptr: Long): Int @JvmStatic external fun sessionJoin(ptr: Long): Int - @JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface) + @JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface?) @JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState) @JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String) @JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService) @@ -350,7 +350,7 @@ class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean) event(RumbleEvent(left.toUByte(), right.toUByte())) } - fun setSurface(surface: Surface) + fun setSurface(surface: Surface?) { ChiakiNative.sessionSetSurface(nativePtr, surface) } diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index d7f127f..dd32a40 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -28,7 +28,8 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va private val _rumbleState = MutableLiveData(RumbleEvent(0U, 0U)) val rumbleState: LiveData get() = _rumbleState - var surfaceTexture: SurfaceTexture? = null + private var surfaceTexture: SurfaceTexture? = null + private var surface: Surface? = null init { @@ -61,9 +62,9 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va _state.value = StreamStateConnecting session.eventCallback = this::eventCallback session.start() - val surfaceTexture = surfaceTexture - if(surfaceTexture != null) - session.setSurface(Surface(surfaceTexture)) + val surface = surface + if(surface != null) + session.setSurface(surface) this.session = session } catch(e: CreateError) @@ -92,6 +93,26 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va } } + fun attachToSurfaceView(surfaceView: SurfaceView) + { + surfaceView.holder.addCallback(object: SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) + { + val surface = holder.surface + this@StreamSession.surface = surface + session?.setSurface(surface) + } + + override fun surfaceDestroyed(holder: SurfaceHolder) + { + this@StreamSession.surface = null + session?.setSurface(null) + } + }) + } + fun attachToTextureView(textureView: TextureView) { textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener { @@ -100,6 +121,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va if(surfaceTexture != null) return surfaceTexture = surface + this@StreamSession.surface = Surface(surfaceTexture) session?.setSurface(Surface(surface)) } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt b/android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt new file mode 100644 index 0000000..5394f02 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt @@ -0,0 +1,68 @@ +package com.metallic.chiaki.stream + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout + +// see ExoPlayer's AspectRatioFrameLayout +class AspectRatioFrameLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null): FrameLayout(context, attrs) +{ + companion object + { + private const val MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f + } + + var aspectRatio = 0f + set(value) + { + if(field != value) + { + field = value + requestLayout() + } + } + + var mode: TransformMode = TransformMode.FIT + set(value) + { + if(field != value) + { + field = value + requestLayout() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) + { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + if(aspectRatio <= 0) + { + // Aspect ratio not set. + return + } + var width = measuredWidth + var height = measuredHeight + val viewAspectRatio = width.toFloat() / height + val aspectDeformation = aspectRatio / viewAspectRatio - 1 + if(Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) + return + when(mode) + { + TransformMode.ZOOM -> + if(aspectDeformation > 0) + width = (height * aspectRatio).toInt() + else + height = (width / aspectRatio).toInt() + TransformMode.FIT -> + if(aspectDeformation > 0) + height = (width / aspectRatio).toInt() + else + width = (height * aspectRatio).toInt() + TransformMode.STRETCH -> {} + } + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + ) + } +} diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index a25772d..32b9fd3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -7,12 +7,12 @@ import android.animation.AnimatorListenerAdapter import android.app.AlertDialog import android.graphics.Matrix import android.os.* -import android.view.KeyEvent -import android.view.MotionEvent -import android.view.TextureView -import android.view.View +import android.transition.TransitionManager +import android.view.* import android.widget.EditText import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -44,6 +44,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private lateinit var viewModel: StreamViewModel private val uiVisibilityHandler = Handler() + private val streamView: View get() = surfaceView override fun onCreate(savedInstanceState: Bundle?) { @@ -94,15 +95,17 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe if (displayModeToggle.checkedButtonId == -1) displayModeToggle.check(checkedId) - adjustTextureViewAspect() + adjustStreamViewAspect() showOverlay() } - viewModel.session.attachToTextureView(textureView) + //viewModel.session.attachToTextureView(textureView) + viewModel.session.attachToSurfaceView(surfaceView) viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) - textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - adjustTextureViewAspect() - } + /*streamView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + adjustStreamViewAspect() + }*/ + adjustStreamViewAspect() if(Preferences(this).rumbleEnabled) { @@ -306,73 +309,84 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } } - private fun adjustTextureViewAspect() + private fun adjustTextureViewAspect(textureView: TextureView) { - val displayInfo = DisplayInfo(viewModel.session.connectInfo.videoProfile, textureView) - val resolution = displayInfo.computeResolutionFor(displayModeToggle.checkedButtonId) - + val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView) + val resolution = trans.resolutionFor(TransformMode.fromButton(displayModeToggle.checkedButtonId)) Matrix().also { textureView.getTransform(it) - it.setScale(resolution.width / displayInfo.viewWidth, resolution.height / displayInfo.viewHeight) - it.postTranslate((displayInfo.viewWidth - resolution.width) * 0.5f, (displayInfo.viewHeight - resolution.height) * 0.5f) + it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight) + it.postTranslate((trans.viewWidth - resolution.width) * 0.5f, (trans.viewHeight - resolution.height) * 0.5f) textureView.setTransform(it) } } + private fun adjustSurfaceViewAspect() + { + val videoProfile = viewModel.session.connectInfo.videoProfile + aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat() + aspectRatioLayout.mode = TransformMode.fromButton(displayModeToggle.checkedButtonId) + } + + private fun adjustStreamViewAspect() = adjustSurfaceViewAspect() + override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event) } - -class DisplayInfo constructor(val videoProfile: ConnectVideoProfile, val textureView: TextureView) +enum class TransformMode { - val contentWidth : Float get() = videoProfile.width.toFloat() - val contentHeight : Float get() = videoProfile.height.toFloat() + FIT, + STRETCH, + ZOOM; + + companion object + { + fun fromButton(displayModeButtonId: Int) + = when (displayModeButtonId) + { + R.id.display_mode_stretch_button -> STRETCH + R.id.display_mode_zoom_button -> ZOOM + else -> FIT + } + } +} + +class TextureViewTransform(private val videoProfile: ConnectVideoProfile, private val textureView: TextureView) +{ + private val contentWidth : Float get() = videoProfile.width.toFloat() + private val contentHeight : Float get() = videoProfile.height.toFloat() val viewWidth : Float get() = textureView.width.toFloat() val viewHeight : Float get() = textureView.height.toFloat() - val contentAspect : Float get() = contentHeight / contentWidth + private val contentAspect : Float get() = contentHeight / contentWidth - fun computeResolutionFor(displayModeButtonId: Int) : Resolution - { - when (displayModeButtonId) + fun resolutionFor(mode: TransformMode): Resolution + = when(mode) { - R.id.display_mode_stretch_button -> return computeStrechedResolution() - R.id.display_mode_zoom_button -> return computeZoomedResolution() - else -> return computeNormalResolution() + TransformMode.STRETCH -> strechedResolution + TransformMode.ZOOM -> zoomedResolution + TransformMode.FIT -> normalResolution } - } - private fun computeStrechedResolution(): Resolution - { - return Resolution(viewWidth, viewHeight) - } + private val strechedResolution get() = Resolution(viewWidth, viewHeight) - private fun computeZoomedResolution(): Resolution - { - if (viewHeight > viewWidth * contentAspect) + private val zoomedResolution get() = + if(viewHeight > viewWidth * contentAspect) { val zoomFactor = viewHeight / contentHeight - return Resolution(contentWidth * zoomFactor, viewHeight) + Resolution(contentWidth * zoomFactor, viewHeight) } else { val zoomFactor = viewWidth / contentWidth - return Resolution(viewWidth, contentHeight * zoomFactor) + Resolution(viewWidth, contentHeight * zoomFactor) } - } - private fun computeNormalResolution(): Resolution - { - if (viewHeight > viewWidth * contentAspect) - { - return Resolution(viewWidth, viewWidth * contentAspect) - } + private val normalResolution get() = + if(viewHeight > viewWidth * contentAspect) + Resolution(viewWidth, viewWidth * contentAspect) else - { - return Resolution(viewHeight / contentAspect, viewHeight) - } - } - + Resolution(viewHeight / contentAspect, viewHeight) } diff --git a/android/app/src/main/res/layout/activity_stream.xml b/android/app/src/main/res/layout/activity_stream.xml index 255ccde..9c944c3 100644 --- a/android/app/src/main/res/layout/activity_stream.xml +++ b/android/app/src/main/res/layout/activity_stream.xml @@ -1,16 +1,24 @@ - - + android:layout_height="match_parent" + android:layout_gravity="center"> + + Date: Thu, 14 Jan 2021 20:44:56 +0100 Subject: [PATCH 047/104] Add nicer LR Buttons to Android --- .../main/res/drawable/control_button_l1.xml | 9 +- .../drawable/control_button_l1_pressed.xml | 9 +- .../main/res/drawable/control_button_l2.xml | 9 +- .../drawable/control_button_l2_pressed.xml | 9 +- .../main/res/drawable/control_button_r1.xml | 9 +- .../drawable/control_button_r1_pressed.xml | 9 +- .../main/res/drawable/control_button_r2.xml | 9 +- .../drawable/control_button_r2_pressed.xml | 9 +- .../src/main/res/layout/fragment_controls.xml | 40 +++--- assets/controls/l1.svg | 56 ++------ assets/controls/l2.svg | 56 ++------ assets/controls/lr12.svg | 124 ++++++++++++++++++ assets/controls/r1.svg | 56 ++------ assets/controls/r2.svg | 55 ++------ 14 files changed, 208 insertions(+), 251 deletions(-) create mode 100644 assets/controls/lr12.svg diff --git a/android/app/src/main/res/drawable/control_button_l1.xml b/android/app/src/main/res/drawable/control_button_l1.xml index a709f25..17f9594 100644 --- a/android/app/src/main/res/drawable/control_button_l1.xml +++ b/android/app/src/main/res/drawable/control_button_l1.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l1_pressed.xml b/android/app/src/main/res/drawable/control_button_l1_pressed.xml index b8bcce0..0267190 100644 --- a/android/app/src/main/res/drawable/control_button_l1_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_l1_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l2.xml b/android/app/src/main/res/drawable/control_button_l2.xml index 6867aa0..8e75f40 100644 --- a/android/app/src/main/res/drawable/control_button_l2.xml +++ b/android/app/src/main/res/drawable/control_button_l2.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l2_pressed.xml b/android/app/src/main/res/drawable/control_button_l2_pressed.xml index e4df2de..b120891 100644 --- a/android/app/src/main/res/drawable/control_button_l2_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_l2_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r1.xml b/android/app/src/main/res/drawable/control_button_r1.xml index a0b3262..d614419 100644 --- a/android/app/src/main/res/drawable/control_button_r1.xml +++ b/android/app/src/main/res/drawable/control_button_r1.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r1_pressed.xml b/android/app/src/main/res/drawable/control_button_r1_pressed.xml index 5cadcb6..8d51983 100644 --- a/android/app/src/main/res/drawable/control_button_r1_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_r1_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r2.xml b/android/app/src/main/res/drawable/control_button_r2.xml index c6a625d..dd16994 100644 --- a/android/app/src/main/res/drawable/control_button_r2.xml +++ b/android/app/src/main/res/drawable/control_button_r2.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r2_pressed.xml b/android/app/src/main/res/drawable/control_button_r2_pressed.xml index 260e60b..7cc84b1 100644 --- a/android/app/src/main/res/drawable/control_button_r2_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_r2_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 7d4842f..3ca53c8 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -171,10 +171,9 @@ + app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent"/> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/l2.svg b/assets/controls/l2.svg index 01e886f..4d561e0 100644 --- a/assets/controls/l2.svg +++ b/assets/controls/l2.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="l2.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/lr12.svg b/assets/controls/lr12.svg new file mode 100644 index 0000000..874475f --- /dev/null +++ b/assets/controls/lr12.svg @@ -0,0 +1,124 @@ + + + + + + + + image/svg+xml + + + + + + + + L1 + + + + R1 + + + + L2 + + + + R2 + + diff --git a/assets/controls/r1.svg b/assets/controls/r1.svg index ef51bc2..70f881c 100644 --- a/assets/controls/r1.svg +++ b/assets/controls/r1.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="r1.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/r2.svg b/assets/controls/r2.svg index 350ffaa..c32639d 100644 --- a/assets/controls/r2.svg +++ b/assets/controls/r2.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="r2.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,13 @@ - From 367489e2307b805c8bec0d072dfdd739a4549a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 14 Jan 2021 20:50:38 +0100 Subject: [PATCH 048/104] Extend Touch Areas on Android --- .../src/main/res/layout/fragment_controls.xml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 3ca53c8..94c2c52 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -148,9 +148,9 @@ Date: Fri, 15 Jan 2021 12:29:56 +0100 Subject: [PATCH 049/104] Update Android Dependencies --- android/app/build.gradle | 14 +++++++------- .../com/metallic/chiaki/stream/StreamActivity.kt | 12 +----------- .../app/src/main/res/layout/activity_stream.xml | 1 + android/build.gradle | 4 ++-- android/gradle/wrapper/gradle-wrapper.properties | 4 ++-- 5 files changed, 13 insertions(+), 22 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index fde2388..2b2c248 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -94,19 +94,19 @@ androidExtensions { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.preference:preference:1.1.0' - implementation 'com.google.android.material:material:1.1.0-beta02' + implementation 'androidx.preference:preference:1.1.1' + implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.2.0' - implementation "io.reactivex.rxjava2:rxjava:2.2.12" + implementation "io.reactivex.rxjava2:rxjava:2.2.20" implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - def room_version = "2.2.4" + def room_version = "2.2.6" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 32b9fd3..9779987 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -44,7 +44,6 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private lateinit var viewModel: StreamViewModel private val uiVisibilityHandler = Handler() - private val streamView: View get() = surfaceView override fun onCreate(savedInstanceState: Bundle?) { @@ -88,13 +87,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe showOverlay() } - displayModeToggle.addOnButtonCheckedListener { _, checkedId, _ -> - // following 'if' is a workaround until selectionRequired for MaterialButtonToggleGroup - // comes out of alpha. - // See https://stackoverflow.com/questions/56164004/required-single-selection-on-materialbuttontogglegroup - if (displayModeToggle.checkedButtonId == -1) - displayModeToggle.check(checkedId) - + displayModeToggle.addOnButtonCheckedListener { _, _, _ -> adjustStreamViewAspect() showOverlay() } @@ -102,9 +95,6 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe //viewModel.session.attachToTextureView(textureView) viewModel.session.attachToSurfaceView(surfaceView) viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) - /*streamView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - adjustStreamViewAspect() - }*/ adjustStreamViewAspect() if(Preferences(this).rumbleEnabled) diff --git a/android/app/src/main/res/layout/activity_stream.xml b/android/app/src/main/res/layout/activity_stream.xml index 9c944c3..99eea1b 100644 --- a/android/app/src/main/res/layout/activity_stream.xml +++ b/android/app/src/main/res/layout/activity_stream.xml @@ -83,6 +83,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:singleSelection="true" + app:selectionRequired="true" app:checkedButton="@id/display_mode_normal_button"> Date: Fri, 15 Jan 2021 14:36:29 +0100 Subject: [PATCH 050/104] Replace Deprecated Android Kotlin Extensions and Others --- android/app/build.gradle | 11 ++-- .../java/com/metallic/chiaki/lib/Chiaki.kt | 3 +- .../main/DisplayHostRecyclerViewAdapter.kt | 11 ++-- .../com/metallic/chiaki/main/MainActivity.kt | 48 +++++++++-------- .../EditManualConsoleActivity.kt | 31 +++++------ .../metallic/chiaki/regist/RegistActivity.kt | 42 ++++++++------- .../chiaki/regist/RegistExecuteActivity.kt | 34 ++++++------ .../metallic/chiaki/session/StreamInput.kt | 3 +- .../chiaki/settings/SettingsActivity.kt | 13 +++-- .../chiaki/settings/SettingsFragment.kt | 1 - .../chiaki/settings/SettingsLogsAdapter.kt | 16 +++--- .../chiaki/settings/SettingsLogsFragment.kt | 20 ++++--- .../SettingsRegisteredHostsAdapter.kt | 16 +++--- .../SettingsRegisteredHostsFragment.kt | 27 ++++++---- .../metallic/chiaki/stream/StreamActivity.kt | 54 +++++++++---------- .../touchcontrols/TouchControlsFragment.kt | 52 +++++++++--------- .../touchcontrols/TouchpadOnlyFragment.kt | 17 ++++-- 17 files changed, 215 insertions(+), 184 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2b2c248..48bf54c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' def rootCMakeLists = "../../CMakeLists.txt" @@ -38,6 +38,9 @@ android { } } } + buildFeatures { + viewBinding true + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -61,6 +64,7 @@ android { } } + Properties properties = new Properties() def propertiesFile = file("../local.properties") if (propertiesFile.exists()) { @@ -86,11 +90,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } } -androidExtensions { - // for @Parcelize - experimental = true -} - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index b8d7204..9533c7c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -3,8 +3,7 @@ package com.metallic.chiaki.lib import android.os.Parcelable import android.util.Log import android.view.Surface -import kotlinx.android.parcel.IgnoredOnParcel -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import java.lang.Exception import java.net.InetSocketAddress import kotlin.math.abs diff --git a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt index 0bd844e..465a2a7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt @@ -3,6 +3,7 @@ package com.metallic.chiaki.main import android.util.Log +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils @@ -16,8 +17,8 @@ import com.metallic.chiaki.common.DiscoveredDisplayHost import com.metallic.chiaki.common.DisplayHost import com.metallic.chiaki.common.ManualDisplayHost import com.metallic.chiaki.common.ext.inflate +import com.metallic.chiaki.databinding.ItemDisplayHostBinding import com.metallic.chiaki.lib.DiscoveryHost -import kotlinx.android.synthetic.main.item_display_host.view.* class DisplayHostDiffCallback(val old: List, val new: List): DiffUtil.Callback() { @@ -42,10 +43,10 @@ class DisplayHostRecyclerViewAdapter( diff.dispatchUpdatesTo(this) } - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemDisplayHostBinding): RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) - = ViewHolder(parent.inflate(R.layout.item_display_host)) + = ViewHolder(ItemDisplayHostBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun getItemCount() = hosts.count() @@ -53,7 +54,7 @@ class DisplayHostRecyclerViewAdapter( { val context = holder.itemView.context val host = hosts[position] - holder.itemView.also { + holder.binding.also { it.nameTextView.text = host.name it.hostTextView.text = context.getString(R.string.display_host_host, host.host) val id = host.id @@ -87,7 +88,7 @@ class DisplayHostRecyclerViewAdapter( else -> R.drawable.ic_console } ) - it.setOnClickListener { clickCallback(host) } + it.root.setOnClickListener { clickCallback(host) } val canWakeup = host.registeredHost != null val canEditDelete = host is ManualDisplayHost diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt index 4fd8178..5bf1f49 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt @@ -17,52 +17,54 @@ import com.metallic.chiaki.R import com.metallic.chiaki.common.* import com.metallic.chiaki.common.ext.putRevealExtra import com.metallic.chiaki.common.ext.viewModelFactory +import com.metallic.chiaki.databinding.ActivityMainBinding import com.metallic.chiaki.lib.ConnectInfo import com.metallic.chiaki.lib.DiscoveryHost import com.metallic.chiaki.manualconsole.EditManualConsoleActivity import com.metallic.chiaki.regist.RegistActivity import com.metallic.chiaki.settings.SettingsActivity import com.metallic.chiaki.stream.StreamActivity -import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel + private lateinit var binding: ActivityMainBinding private var discoveryMenuItem: MenuItem? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) title = "" - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) - floatingActionButton.setOnClickListener { - expandFloatingActionButton(!floatingActionButton.isExpanded) + binding.floatingActionButton.setOnClickListener { + expandFloatingActionButton(!binding.floatingActionButton.isExpanded) } - floatingActionButtonDialBackground.setOnClickListener { + binding.floatingActionButtonDialBackground.setOnClickListener { expandFloatingActionButton(false) } - addManualButton.setOnClickListener { addManualConsole() } - addManualLabelButton.setOnClickListener { addManualConsole() } + binding.addManualButton.setOnClickListener { addManualConsole() } + binding.addManualLabelButton.setOnClickListener { addManualConsole() } - registerButton.setOnClickListener { showRegistration() } - registerLabelButton.setOnClickListener { showRegistration() } + binding.registerButton.setOnClickListener { showRegistration() } + binding.registerLabelButton.setOnClickListener { showRegistration() } viewModel = ViewModelProvider(this, viewModelFactory { MainViewModel(getDatabase(this), Preferences(this)) }) .get(MainViewModel::class.java) val recyclerViewAdapter = DisplayHostRecyclerViewAdapter(this::hostTriggered, this::wakeupHost, this::editHost, this::deleteHost) - hostsRecyclerView.adapter = recyclerViewAdapter - hostsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.hostsRecyclerView.adapter = recyclerViewAdapter + binding.hostsRecyclerView.layoutManager = LinearLayoutManager(this) viewModel.displayHosts.observe(this, Observer { - val top = hostsRecyclerView.computeVerticalScrollOffset() == 0 + val top = binding.hostsRecyclerView.computeVerticalScrollOffset() == 0 recyclerViewAdapter.hosts = it if(top) - hostsRecyclerView.scrollToPosition(0) + binding.hostsRecyclerView.scrollToPosition(0) updateEmptyInfo() }) @@ -76,19 +78,19 @@ class MainActivity : AppCompatActivity() { if(viewModel.displayHosts.value?.isEmpty() ?: true) { - emptyInfoLayout.visibility = View.VISIBLE + binding.emptyInfoLayout.visibility = View.VISIBLE val discoveryActive = viewModel.discoveryActive.value ?: false - emptyInfoImageView.setImageResource(if(discoveryActive) R.drawable.ic_discover_on else R.drawable.ic_discover_off) - emptyInfoTextView.setText(if(discoveryActive) R.string.display_hosts_empty_discovery_on_info else R.string.display_hosts_empty_discovery_off_info) + binding.emptyInfoImageView.setImageResource(if(discoveryActive) R.drawable.ic_discover_on else R.drawable.ic_discover_off) + binding.emptyInfoTextView.setText(if(discoveryActive) R.string.display_hosts_empty_discovery_on_info else R.string.display_hosts_empty_discovery_off_info) } else - emptyInfoLayout.visibility = View.GONE + binding.emptyInfoLayout.visibility = View.GONE } private fun expandFloatingActionButton(expand: Boolean) { - floatingActionButton.isExpanded = expand - floatingActionButton.isActivated = floatingActionButton.isExpanded + binding.floatingActionButton.isExpanded = expand + binding.floatingActionButton.isActivated = binding.floatingActionButton.isExpanded } override fun onStart() @@ -105,7 +107,7 @@ class MainActivity : AppCompatActivity() override fun onBackPressed() { - if(floatingActionButton.isExpanded) + if(binding.floatingActionButton.isExpanded) { expandFloatingActionButton(false) return @@ -151,7 +153,7 @@ class MainActivity : AppCompatActivity() private fun addManualConsole() { Intent(this, EditManualConsoleActivity::class.java).also { - it.putRevealExtra(addManualButton, rootLayout) + it.putRevealExtra(binding.addManualButton, binding.rootLayout) startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } @@ -159,7 +161,7 @@ class MainActivity : AppCompatActivity() private fun showRegistration() { Intent(this, RegistActivity::class.java).also { - it.putRevealExtra(registerButton, rootLayout) + it.putRevealExtra(binding.registerButton, binding.rootLayout) startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt index 49e5f4d..bb64b87 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt @@ -16,10 +16,10 @@ import com.metallic.chiaki.common.RegisteredHost import com.metallic.chiaki.common.ext.RevealActivity import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase +import com.metallic.chiaki.databinding.ActivityEditManualBinding import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo -import kotlinx.android.synthetic.main.activity_edit_manual.* class EditManualConsoleActivity: AppCompatActivity(), RevealActivity { @@ -28,18 +28,20 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity const val EXTRA_MANUAL_HOST_ID = "manual_host_id" } - override val revealIntent: Intent get() = intent - override val revealRootLayout: View get() = rootLayout - override val revealWindow: Window get() = window - private lateinit var viewModel: EditManualConsoleViewModel + private lateinit var binding: ActivityEditManualBinding + + override val revealIntent: Intent get() = intent + override val revealRootLayout: View get() = binding.rootLayout + override val revealWindow: Window get() = window private val disposable = CompositeDisposable() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_edit_manual) + binding = ActivityEditManualBinding.inflate(layoutInflater) + setContentView(binding.root) handleReveal() viewModel = ViewModelProvider(this, viewModelFactory { @@ -52,17 +54,17 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity .get(EditManualConsoleViewModel::class.java) viewModel.existingHost?.observe(this, Observer { - hostEditText.setText(it.host) + binding.hostEditText.setText(it.host) }) viewModel.selectedRegisteredHost.observe(this, Observer { - registeredHostTextView.setText(titleForRegisteredHost(it)) + binding.registeredHostTextView.setText(titleForRegisteredHost(it)) }) viewModel.registeredHosts.observe(this, Observer { hosts -> - registeredHostTextView.setAdapter(ArrayAdapter(this, R.layout.dropdown_menu_popup_item, + binding.registeredHostTextView.setAdapter(ArrayAdapter(this, R.layout.dropdown_menu_popup_item, hosts.map { titleForRegisteredHost(it) })) - registeredHostTextView.onItemClickListener = object: AdapterView.OnItemClickListener { + binding.registeredHostTextView.onItemClickListener = object: AdapterView.OnItemClickListener { override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { if(position >= hosts.size) @@ -73,8 +75,7 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity } }) - - saveButton.setOnClickListener { saveHost() } + binding.saveButton.setOnClickListener { saveHost() } } private fun titleForRegisteredHost(registeredHost: RegisteredHost?) = @@ -85,14 +86,14 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity private fun saveHost() { - val host = hostEditText.text.toString().trim() + val host = binding.hostEditText.text.toString().trim() if(host.isEmpty()) { - hostEditText.error = getString(R.string.entered_host_invalid) + binding.hostEditText.error = getString(R.string.entered_host_invalid) return } - saveButton.isEnabled = false + binding.saveButton.isEnabled = false viewModel.saveHost(host) .observeOn(AndroidSchedulers.mainThread()) .subscribe { diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index fb3ddaa..a8d06f6 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -12,9 +12,9 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import com.metallic.chiaki.R import com.metallic.chiaki.common.ext.RevealActivity +import com.metallic.chiaki.databinding.ActivityRegistBinding import com.metallic.chiaki.lib.RegistInfo import com.metallic.chiaki.lib.Target -import kotlinx.android.synthetic.main.activity_regist.* import java.lang.IllegalArgumentException class RegistActivity: AppCompatActivity(), RevealActivity @@ -30,33 +30,35 @@ class RegistActivity: AppCompatActivity(), RevealActivity private const val REQUEST_REGIST = 1 } + private lateinit var viewModel: RegistViewModel + private lateinit var binding: ActivityRegistBinding + override val revealWindow: Window get() = window override val revealIntent: Intent get() = intent - override val revealRootLayout: View get() = rootLayout - - private lateinit var viewModel: RegistViewModel + override val revealRootLayout: View get() = binding.rootLayout override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + binding = ActivityRegistBinding.inflate(layoutInflater) setContentView(R.layout.activity_regist) handleReveal() viewModel = ViewModelProvider(this).get(RegistViewModel::class.java) - hostEditText.setText(intent.getStringExtra(EXTRA_HOST) ?: "255.255.255.255") - broadcastCheckBox.isChecked = intent.getBooleanExtra(EXTRA_BROADCAST, true) + binding.hostEditText.setText(intent.getStringExtra(EXTRA_HOST) ?: "255.255.255.255") + binding.broadcastCheckBox.isChecked = intent.getBooleanExtra(EXTRA_BROADCAST, true) - registButton.setOnClickListener { doRegist() } + binding.registButton.setOnClickListener { doRegist() } - ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) { + binding.ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) { RegistViewModel.ConsoleVersion.PS5 -> R.id.ps5RadioButton RegistViewModel.ConsoleVersion.PS4_GE_8 -> R.id.ps4VersionGE8RadioButton RegistViewModel.ConsoleVersion.PS4_GE_7 -> R.id.ps4VersionGE7RadioButton RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.id.ps4VersionLT7RadioButton }) - ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> + binding.ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> viewModel.ps4Version.value = when(checkedId) { R.id.ps5RadioButton -> RegistViewModel.ConsoleVersion.PS5 @@ -68,14 +70,14 @@ class RegistActivity: AppCompatActivity(), RevealActivity } viewModel.ps4Version.observe(this, Observer { - psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE - psnIdTextInputLayout.hint = getString(when(it!!) + binding.psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE + binding.psnIdTextInputLayout.hint = getString(when(it!!) { RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.hint_regist_psn_online_id else -> R.string.hint_regist_psn_account_id }) - pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before) - pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation) + binding.pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before) + binding.pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation) }) } @@ -83,11 +85,11 @@ class RegistActivity: AppCompatActivity(), RevealActivity { val ps4Version = viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5 - val host = hostEditText.text.toString().trim() + val host = binding.hostEditText.text.toString().trim() val hostValid = host.isNotEmpty() - val broadcast = broadcastCheckBox.isChecked + val broadcast = binding.broadcastCheckBox.isChecked - val psnId = psnIdEditText.text.toString().trim() + val psnId = binding.psnIdEditText.text.toString().trim() val psnOnlineId: String? = if(ps4Version == RegistViewModel.ConsoleVersion.PS4_LT_7) psnId else null val psnAccountId: ByteArray? = if(ps4Version != RegistViewModel.ConsoleVersion.PS4_LT_7) @@ -101,11 +103,11 @@ class RegistActivity: AppCompatActivity(), RevealActivity } - val pin = pinEditText.text.toString() + val pin = binding.pinEditText.text.toString() val pinValid = pin.length == PIN_LENGTH - hostEditText.error = if(!hostValid) getString(R.string.entered_host_invalid) else null - psnIdEditText.error = + binding.hostEditText.error = if(!hostValid) getString(R.string.entered_host_invalid) else null + binding.psnIdEditText.error = if(!psnIdValid) getString(when(ps4Version) { @@ -114,7 +116,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity }) else null - pinEditText.error = if(!pinValid) getString(R.string.regist_pin_invalid, PIN_LENGTH) else null + binding.pinEditText.error = if(!pinValid) getString(R.string.regist_pin_invalid, PIN_LENGTH) else null if(!hostValid || !psnIdValid || !pinValid) return diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt index 870b67e..16f545a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt @@ -16,8 +16,8 @@ import com.metallic.chiaki.R import com.metallic.chiaki.common.MacAddress import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase +import com.metallic.chiaki.databinding.ActivityRegistExecuteBinding import com.metallic.chiaki.lib.RegistInfo -import kotlinx.android.synthetic.main.activity_regist_execute.* import kotlin.math.max class RegistExecuteActivity: AppCompatActivity() @@ -31,55 +31,57 @@ class RegistExecuteActivity: AppCompatActivity() } private lateinit var viewModel: RegistExecuteViewModel + private lateinit var binding: ActivityRegistExecuteBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_regist_execute) + binding = ActivityRegistExecuteBinding.inflate(layoutInflater) + setContentView(binding.root) viewModel = ViewModelProvider(this, viewModelFactory { RegistExecuteViewModel(getDatabase(this)) }) .get(RegistExecuteViewModel::class.java) - logTextView.setHorizontallyScrolling(true) - logTextView.movementMethod = ScrollingMovementMethod() + binding.logTextView.setHorizontallyScrolling(true) + binding.logTextView.movementMethod = ScrollingMovementMethod() viewModel.logText.observe(this, Observer { - val textLayout = logTextView.layout ?: return@Observer + val textLayout = binding.logTextView.layout ?: return@Observer val lineCount = textLayout.lineCount if(lineCount < 1) return@Observer - logTextView.text = it - val scrollY = textLayout.getLineBottom(lineCount - 1) - logTextView.height + logTextView.paddingTop + logTextView.paddingBottom - logTextView.scrollTo(0, max(scrollY, 0)) + binding.logTextView.text = it + val scrollY = textLayout.getLineBottom(lineCount - 1) - binding.logTextView.height + binding.logTextView.paddingTop + binding.logTextView.paddingBottom + binding.logTextView.scrollTo(0, max(scrollY, 0)) }) viewModel.state.observe(this, Observer { - progressBar.visibility = if(it == RegistExecuteViewModel.State.RUNNING) View.VISIBLE else View.GONE + binding.progressBar.visibility = if(it == RegistExecuteViewModel.State.RUNNING) View.VISIBLE else View.GONE when(it) { RegistExecuteViewModel.State.FAILED -> { - infoTextView.visibility = View.VISIBLE - infoTextView.setText(R.string.regist_info_failed) + binding.infoTextView.visibility = View.VISIBLE + binding.infoTextView.setText(R.string.regist_info_failed) setResult(RESULT_FAILED) } RegistExecuteViewModel.State.SUCCESSFUL, RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE -> { - infoTextView.visibility = View.VISIBLE - infoTextView.setText(R.string.regist_info_success) + binding.infoTextView.visibility = View.VISIBLE + binding.infoTextView.setText(R.string.regist_info_success) setResult(RESULT_OK) if(it == RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE) showDuplicateDialog() } RegistExecuteViewModel.State.STOPPED -> { - infoTextView.visibility = View.GONE + binding.infoTextView.visibility = View.GONE setResult(Activity.RESULT_CANCELED) } - else -> infoTextView.visibility = View.GONE + else -> binding.infoTextView.visibility = View.GONE } }) - shareLogButton.setOnClickListener { + binding.shareLogButton.setOnClickListener { val log = viewModel.logText.value ?: "" Intent(Intent.ACTION_SEND).also { it.type = "text/plain" diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt index 2518ce1..6fbfc31 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt @@ -19,6 +19,7 @@ class StreamInput(val context: Context, val preferences: Preferences) val controllerState = sensorControllerState or keyControllerState or motionControllerState val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + @Suppress("DEPRECATION") when(windowManager.defaultDisplay.rotation) { Surface.ROTATION_90 -> { @@ -175,7 +176,7 @@ class StreamInput(val context: Context, val preferences: Preferences) { if(event.source and InputDevice.SOURCE_CLASS_JOYSTICK != InputDevice.SOURCE_CLASS_JOYSTICK) return false - fun Float.signedAxis() = (this * Short.MAX_VALUE).toShort() + fun Float.signedAxis() = (this * Short.MAX_VALUE).toInt().toShort() fun Float.unsignedAxis() = (this * UByte.MAX_VALUE.toFloat()).toUInt().toUByte() motionControllerState.leftX = event.getAxisValue(MotionEvent.AXIS_X).signedAxis() motionControllerState.leftY = event.getAxisValue(MotionEvent.AXIS_Y).signedAxis() diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt index 3a87e1b..1cf224f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt @@ -9,7 +9,7 @@ import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.metallic.chiaki.R -import kotlinx.android.synthetic.main.activity_settings.* +import com.metallic.chiaki.databinding.ActivitySettingsBinding interface TitleFragment { @@ -18,20 +18,23 @@ interface TitleFragment class SettingsActivity: AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + private lateinit var binding: ActivitySettingsBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) title = "" - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) val rootFragment = SettingsFragment() replaceFragment(rootFragment, false) supportFragmentManager.addOnBackStackChangedListener { val titleFragment = supportFragmentManager.findFragmentById(R.id.settingsFragment) as? TitleFragment ?: return@addOnBackStackChangedListener - titleTextView.text = titleFragment.getTitle(resources) + binding.titleTextView.text = titleFragment.getTitle(resources) } - titleTextView.text = rootFragment.getTitle(resources) + binding.titleTextView.text = rootFragment.getTitle(resources) } override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference) = when(pref.fragment) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 7a4506e..957d1d5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -16,7 +16,6 @@ import com.metallic.chiaki.common.exportAndShareAllSettings import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase import com.metallic.chiaki.common.importSettingsFromUri -import com.metallic.chiaki.lib.Codec import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt index 46c8904..6acf547 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt @@ -2,13 +2,13 @@ package com.metallic.chiaki.settings -import android.view.View +import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.metallic.chiaki.R import com.metallic.chiaki.common.LogFile import com.metallic.chiaki.common.ext.inflate -import kotlinx.android.synthetic.main.item_log_file.view.* +import com.metallic.chiaki.databinding.ItemLogFileBinding import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -20,7 +20,7 @@ class SettingsLogsAdapter: RecyclerView.Adapter( private val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) private val timeFormat = SimpleDateFormat("HH:mm:ss:SSS", Locale.getDefault()) - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemLogFileBinding): RecyclerView.ViewHolder(binding.root) var logFiles: List = listOf() set(value) @@ -29,16 +29,16 @@ class SettingsLogsAdapter: RecyclerView.Adapter( notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(parent.inflate(R.layout.item_log_file)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + ViewHolder(ItemLogFileBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun getItemCount() = logFiles.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val view = holder.itemView val logFile = logFiles[position] - view.nameTextView.text = "${dateFormat.format(logFile.date)} ${timeFormat.format(logFile.date)}" - view.summaryTextView.text = logFile.filename - view.shareButton.setOnClickListener { shareCallback?.let { it(logFile) } } + holder.binding.nameTextView.text = "${dateFormat.format(logFile.date)} ${timeFormat.format(logFile.date)}" + holder.binding.summaryTextView.text = logFile.filename + holder.binding.shareButton.setOnClickListener { shareCallback?.let { it(logFile) } } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt index 07e1e3f..0397172 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt @@ -21,29 +21,35 @@ import com.metallic.chiaki.common.LogFile import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.fileProviderAuthority -import kotlinx.android.synthetic.main.fragment_settings_logs.* +import com.metallic.chiaki.databinding.FragmentSettingsLogsBinding class SettingsLogsFragment: AppCompatDialogFragment(), TitleFragment { private lateinit var viewModel: SettingsLogsViewModel + private var _binding: FragmentSettingsLogsBinding? = null + private val binding get() = _binding!! + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - inflater.inflate(R.layout.fragment_settings_logs, container, false) + FragmentSettingsLogsBinding.inflate(inflater, container, false).let { + _binding = it + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val context = context!! + val context = requireContext() viewModel = ViewModelProvider(this, viewModelFactory { SettingsLogsViewModel(LogManager(context)) }) .get(SettingsLogsViewModel::class.java) val adapter = SettingsLogsAdapter() - logsRecyclerView.layoutManager = LinearLayoutManager(context) - logsRecyclerView.adapter = adapter + binding.logsRecyclerView.layoutManager = LinearLayoutManager(context) + binding.logsRecyclerView.adapter = adapter adapter.shareCallback = this::shareLogFile viewModel.sessionLogs.observe(viewLifecycleOwner, Observer { adapter.logFiles = it - emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE + binding.emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE }) val itemTouchSwipeCallback = object : ItemTouchSwipeCallback(context) @@ -55,7 +61,7 @@ class SettingsLogsFragment: AppCompatDialogFragment(), TitleFragment viewModel.deleteLog(file) } } - ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(logsRecyclerView) + ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(binding.logsRecyclerView) } override fun getTitle(resources: Resources): String = resources.getString(R.string.preferences_logs_title) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt index 57a06ce..509d594 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt @@ -2,17 +2,15 @@ package com.metallic.chiaki.settings -import android.view.View +import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.metallic.chiaki.R import com.metallic.chiaki.common.RegisteredHost -import com.metallic.chiaki.common.ext.inflate -import kotlinx.android.synthetic.main.item_registered_host.view.* +import com.metallic.chiaki.databinding.ItemRegisteredHostBinding class SettingsRegisteredHostsAdapter: RecyclerView.Adapter() { - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemRegisteredHostBinding): RecyclerView.ViewHolder(binding.root) var hosts: List = listOf() set(value) @@ -21,15 +19,15 @@ class SettingsRegisteredHostsAdapter: RecyclerView.Adapter + binding.onScreenControlsSwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setOnScreenControlsEnabled(isChecked) showOverlay() } viewModel.touchpadOnlyEnabled.observe(this, Observer { - if(touchpadOnlySwitch.isChecked != it) - touchpadOnlySwitch.isChecked = it - if(touchpadOnlySwitch.isChecked) - onScreenControlsSwitch.isChecked = false + if(binding.touchpadOnlySwitch.isChecked != it) + binding.touchpadOnlySwitch.isChecked = it + if(binding.touchpadOnlySwitch.isChecked) + binding.onScreenControlsSwitch.isChecked = false }) - touchpadOnlySwitch.setOnCheckedChangeListener { _, isChecked -> + binding.touchpadOnlySwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setTouchpadOnlyEnabled(isChecked) showOverlay() } - displayModeToggle.addOnButtonCheckedListener { _, _, _ -> + binding.displayModeToggle.addOnButtonCheckedListener { _, _, _ -> adjustStreamViewAspect() showOverlay() } //viewModel.session.attachToTextureView(textureView) - viewModel.session.attachToSurfaceView(surfaceView) + viewModel.session.attachToSurfaceView(binding.surfaceView) viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) adjustStreamViewAspect() @@ -159,14 +159,14 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun showOverlay() { - overlay.isVisible = true - overlay.animate() + binding.overlay.isVisible = true + binding.overlay.animate() .alpha(1.0f) .setListener(object: AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { - overlay.alpha = 1.0f + binding.overlay.alpha = 1.0f } }) uiVisibilityHandler.removeCallbacks(hideSystemUIRunnable) @@ -175,13 +175,13 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun hideOverlay() { - overlay.animate() + binding.overlay.animate() .alpha(0.0f) .setListener(object: AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { - overlay.isGone = true + binding.overlay.isGone = true } }) } @@ -214,7 +214,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun stateChanged(state: StreamState) { - progressBar.visibility = if(state == StreamStateConnecting) View.VISIBLE else View.GONE + binding.progressBar.visibility = if(state == StreamStateConnecting) View.VISIBLE else View.GONE when(state) { @@ -302,7 +302,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun adjustTextureViewAspect(textureView: TextureView) { val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView) - val resolution = trans.resolutionFor(TransformMode.fromButton(displayModeToggle.checkedButtonId)) + val resolution = trans.resolutionFor(TransformMode.fromButton(binding.displayModeToggle.checkedButtonId)) Matrix().also { textureView.getTransform(it) it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight) @@ -314,8 +314,8 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun adjustSurfaceViewAspect() { val videoProfile = viewModel.session.connectInfo.videoProfile - aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat() - aspectRatioLayout.mode = TransformMode.fromButton(displayModeToggle.checkedButtonId) + binding.aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat() + binding.aspectRatioLayout.mode = TransformMode.fromButton(binding.displayModeToggle.checkedButtonId) } private fun adjustStreamViewAspect() = adjustSurfaceViewAspect() diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index 644f5f3..d3ab155 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -3,16 +3,14 @@ package com.metallic.chiaki.touchcontrols import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.metallic.chiaki.R +import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.lib.ControllerState -import kotlinx.android.synthetic.main.fragment_controls.* class TouchControlsFragment : Fragment() { @@ -28,44 +26,50 @@ class TouchControlsFragment : Fragment() var controllerStateCallback: ((ControllerState) -> Unit)? = null var onScreenControlsEnabled: LiveData? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View - = inflater.inflate(R.layout.fragment_controls, container, false) + private var _binding: FragmentControlsBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + FragmentControlsBinding.inflate(inflater, container, false).let { + _binding = it + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - dpadView.stateChangeCallback = this::dpadStateChanged - crossButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_CROSS) - moonButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_MOON) - pyramidButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PYRAMID) - boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) - l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) - r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) - l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3) - r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3) - optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) - shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) - psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) - touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) + binding.dpadView.stateChangeCallback = this::dpadStateChanged + binding.crossButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_CROSS) + binding.moonButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_MOON) + binding.pyramidButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PYRAMID) + binding.boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) + binding.l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) + binding.r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) + binding.l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3) + binding.r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3) + binding.optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) + binding.shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) + binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) + binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } - r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } + binding.l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } + binding.r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } val quantizeStick = { f: Float -> - (Short.MAX_VALUE * f).toShort() + (Short.MAX_VALUE * f).toInt().toShort() } - leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { leftX = quantizeStick(it.x) leftY = quantizeStick(it.y) }} - rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { rightX = quantizeStick(it.x) rightY = quantizeStick(it.y) }} - onScreenControlsEnabled?.observe(this, Observer { + onScreenControlsEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index 610c099..ef96b84 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -10,8 +10,9 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.metallic.chiaki.R +import com.metallic.chiaki.databinding.FragmentControlsBinding +import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding import com.metallic.chiaki.lib.ControllerState -import kotlinx.android.synthetic.main.fragment_controls.* class TouchpadOnlyFragment : Fragment() { @@ -27,16 +28,22 @@ class TouchpadOnlyFragment : Fragment() var controllerStateCallback: ((ControllerState) -> Unit)? = null var touchpadOnlyEnabled: LiveData? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View - = inflater.inflate(R.layout.fragment_touchpad_only, container, false) + private var _binding: FragmentTouchpadOnlyBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + FragmentTouchpadOnlyBinding.inflate(inflater, container, false).let { + _binding = it + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) + binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - touchpadOnlyEnabled?.observe(this, Observer { + touchpadOnlyEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) } From 5914ceec77aa545e4995377f30d4c80d0cef3260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 11:34:10 +0100 Subject: [PATCH 051/104] Add TouchpadView to Android --- .../java/com/metallic/chiaki/lib/Chiaki.kt | 38 +++++++- .../chiaki/touchcontrols/TouchTracker.kt | 2 +- .../chiaki/touchcontrols/TouchpadView.kt | 86 +++++++++++++++++++ .../main/res/drawable/control_touchpad.xml | 12 +++ .../src/main/res/layout/fragment_controls.xml | 10 +++ android/app/src/main/res/values/attrs.xml | 4 + assets/controls/touchpad_surface.svg | 65 ++++++++++++++ 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt create mode 100644 android/app/src/main/res/drawable/control_touchpad.xml create mode 100644 assets/controls/touchpad_surface.svg diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 9533c7c..9c4ea34 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -152,9 +152,9 @@ private fun maxAbs(a: Short, b: Short) = if(abs(a.toInt()) > abs(b.toInt())) a e private val CONTROLLER_TOUCHES_MAX = 2 // must be the same as CHIAKI_CONTROLLER_TOUCHES_MAX data class ControllerTouch( - val x: UShort = 0U, - val y: UShort = 0U, - val id: Byte = -1 // -1 = up + var x: UShort = 0U, + var y: UShort = 0U, + var id: Byte = -1 // -1 = up ) data class ControllerState constructor( @@ -165,7 +165,7 @@ data class ControllerState constructor( var leftY: Short = 0, var rightX: Short = 0, var rightY: Short = 0, - private var touchIdNext: UByte = 0U, + private var touchIdNext: UByte = 100U, var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), var gyroX: Float = 0.0f, var gyroY: Float = 0.0f, @@ -196,6 +196,8 @@ data class ControllerState constructor( val BUTTON_SHARE = (1 shl 13).toUInt() val BUTTON_TOUCHPAD = (1 shl 14).toUInt() val BUTTON_PS = (1 shl 15).toUInt() + val TOUCHPAD_WIDTH: UShort = 1920U + val TOUCHPAD_HEIGHT: UShort = 942U } infix fun or(o: ControllerState) = ControllerState( @@ -272,6 +274,34 @@ data class ControllerState constructor( result = 31 * result + orientW.hashCode() return result } + + fun startTouch(x: UShort, y: UShort): UByte? = + touches + .find { it.id < 0 } + ?.also { + it.id = touchIdNext.toByte() + Log.d("TouchId", "touch id next: $touchIdNext") + touchIdNext = ((touchIdNext + 1U) and 0x7fU).toUByte() + }?.id?.toUByte() + + fun stopTouch(id: UByte) + { + touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + it.id = -1 + } + } + + fun setTouchPos(id: UByte, x: UShort, y: UShort): Boolean + = touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + val r = it.x != x || it.y != y + it.x = x + it.y = y + r + } ?: false } class QuitReason(val value: Int) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt index 01d6869..a1ead71 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt @@ -26,7 +26,7 @@ class TouchTracker if(pointerId == null) { pointerId = event.getPointerId(event.actionIndex) - currentPosition = Vector(event.x, event.y) + currentPosition = Vector(event.getX(event.actionIndex), event.getY(event.actionIndex)) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt new file mode 100644 index 0000000..292351b --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.metallic.chiaki.R +import com.metallic.chiaki.lib.ControllerState + +class TouchpadView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) +{ + val state: ControllerState = ControllerState() + private val pointerTouchIds = mutableMapOf() + + var stateChangeCallback: ((ControllerState) -> Unit)? = null + + private val drawable: Drawable? + + init + { + context.theme.obtainStyledAttributes(attrs, R.styleable.TouchpadView, 0, 0).apply { + drawable = getDrawable(R.styleable.TouchpadView_drawable) + recycle() + } + isClickable = true + } + + override fun onDraw(canvas: Canvas) + { + super.onDraw(canvas) + if(state.touches.find { it.id >= 0 } == null) + return + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) + drawable?.draw(canvas) + } + + private fun touchX(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_WIDTH - 1u).toUShort(), + (ControllerState.TOUCHPAD_WIDTH.toFloat() * event.getX(index) / width.toFloat()).toUInt().toUShort())) + + private fun touchY(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_HEIGHT - 1u).toUShort(), + (ControllerState.TOUCHPAD_HEIGHT.toFloat() * event.getY(index) / height.toFloat()).toUInt().toUShort())) + + override fun onTouchEvent(event: MotionEvent): Boolean + { + when(event.actionMasked) + { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { + pointerTouchIds[event.getPointerId(event.actionIndex)] = it + triggerStateChanged() + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + pointerTouchIds.remove(event.getPointerId(event.actionIndex))?.let { + state.stopTouch(it) + triggerStateChanged() + } + } + MotionEvent.ACTION_MOVE -> { + val changed = pointerTouchIds.entries.fold(false) { acc, it -> + val index = event.findPointerIndex(it.key) + if(index < 0) + acc + else + acc || state.setTouchPos(it.value, touchX(event, index), touchY(event, index)) + } + if(changed) + triggerStateChanged() + } + } + return true + } + + private fun triggerStateChanged() + { + stateChangeCallback?.let { it(state) } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/control_touchpad.xml b/android/app/src/main/res/drawable/control_touchpad.xml new file mode 100644 index 0000000..509b8a3 --- /dev/null +++ b/android/app/src/main/res/drawable/control_touchpad.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 94c2c52..1f33197 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -47,6 +47,16 @@ app:drawableHandle="@drawable/control_analog_stick_handle" /> + + + + + + \ No newline at end of file diff --git a/assets/controls/touchpad_surface.svg b/assets/controls/touchpad_surface.svg new file mode 100644 index 0000000..7476128 --- /dev/null +++ b/assets/controls/touchpad_surface.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + + + From fa44a3269c1e9ba6120adb343d9bb555c95ff2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 15:32:37 +0100 Subject: [PATCH 052/104] Propagate Touchpad State through TouchControlsFragment on Android --- .../metallic/chiaki/stream/StreamActivity.kt | 31 ++++++++++++----- .../touchcontrols/TouchControlsFragment.kt | 33 ++++++++++++++----- .../chiaki/touchcontrols/TouchpadView.kt | 11 +++++-- .../src/main/res/layout/fragment_controls.xml | 1 + 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index def22f4..fd07ff4 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -24,6 +24,8 @@ import com.metallic.chiaki.lib.ConnectVideoProfile import com.metallic.chiaki.session.* import com.metallic.chiaki.touchcontrols.TouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo import kotlin.math.min private sealed class DialogContents @@ -113,18 +115,25 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } } + private val controlsDisposable = CompositeDisposable() + override fun onAttachFragment(fragment: Fragment) { super.onAttachFragment(fragment) - if(fragment is TouchControlsFragment) + when(fragment) { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } - fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled - } - if(fragment is TouchpadOnlyFragment) - { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } - fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled + is TouchControlsFragment -> + { + fragment.controllerState + .subscribe { viewModel.input.touchControllerState = it } + .addTo(controlsDisposable) + fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled + } + is TouchpadOnlyFragment -> + { + fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } + fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled + } } } @@ -141,6 +150,12 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe viewModel.session.pause() } + override fun onDestroy() + { + super.onDestroy() + controlsDisposable.dispose() + } + private fun reconnect() { viewModel.session.shutdown() diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index d3ab155..ad734ce 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -11,19 +11,31 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.lib.ControllerState +import io.reactivex.Observable +import io.reactivex.Observable.combineLatest +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject class TouchControlsFragment : Fragment() { - private var controllerState = ControllerState() + private var ownControllerState = ControllerState() private set(value) { val diff = field != value field = value if(diff) - controllerStateCallback?.let { it(value) } + ownControllerStateSubject.onNext(ownControllerState) } - var controllerStateCallback: ((ControllerState) -> Unit)? = null + private val ownControllerStateSubject: Subject + = BehaviorSubject.create().also { it.onNext(ownControllerState) } + + // to delay attaching to the touchpadView until it's available + private val controllerStateProxy: Subject> + = BehaviorSubject.create>().also { it.onNext(ownControllerStateSubject) } + val controllerState: Observable get() = + controllerStateProxy.flatMap { it } + var onScreenControlsEnabled: LiveData? = null private var _binding: FragmentControlsBinding? = null @@ -32,6 +44,9 @@ class TouchControlsFragment : Fragment() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = FragmentControlsBinding.inflate(inflater, container, false).let { _binding = it + controllerStateProxy.onNext( + combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b } + ) it.root } @@ -52,19 +67,19 @@ class TouchControlsFragment : Fragment() binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - binding.l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } - binding.r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } + binding.l2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { l2State = if(it) 255U else 0U } } + binding.r2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { r2State = if(it) 255U else 0U } } val quantizeStick = { f: Float -> (Short.MAX_VALUE * f).toInt().toShort() } - binding.leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.leftAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply { leftX = quantizeStick(it.x) leftY = quantizeStick(it.y) }} - binding.rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.rightAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply { rightX = quantizeStick(it.x) rightY = quantizeStick(it.y) }} @@ -76,7 +91,7 @@ class TouchControlsFragment : Fragment() private fun dpadStateChanged(direction: DPadView.Direction?) { - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = ((buttons and ControllerState.BUTTON_DPAD_LEFT.inv() and ControllerState.BUTTON_DPAD_RIGHT.inv() @@ -98,7 +113,7 @@ class TouchControlsFragment : Fragment() } private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean -> - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = if(pressed) buttons or buttonMask diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index 292351b..8c8f44f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -10,15 +10,20 @@ import android.view.MotionEvent import android.view.View import com.metallic.chiaki.R import com.metallic.chiaki.lib.ControllerState +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject class TouchpadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { - val state: ControllerState = ControllerState() + private val state: ControllerState = ControllerState() private val pointerTouchIds = mutableMapOf() - var stateChangeCallback: ((ControllerState) -> Unit)? = null + private val stateSubject: Subject + = BehaviorSubject.create().also { it.onNext(state) } + val controllerState: Observable get() = stateSubject private val drawable: Drawable? @@ -81,6 +86,6 @@ class TouchpadView @JvmOverloads constructor( private fun triggerStateChanged() { - stateChangeCallback?.let { it(state) } + stateSubject.onNext(state) } } \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 1f33197..ee2a4a8 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -48,6 +48,7 @@ /> Date: Fri, 15 Jan 2021 17:00:05 +0100 Subject: [PATCH 053/104] Finish Basic Touchpad on Android --- .../java/com/metallic/chiaki/lib/Chiaki.kt | 5 +++-- .../metallic/chiaki/stream/StreamActivity.kt | 19 ++++++---------- .../touchcontrols/TouchControlsFragment.kt | 17 ++++++++------ .../touchcontrols/TouchpadOnlyFragment.kt | 22 +++++-------------- .../chiaki/touchcontrols/TouchpadView.kt | 1 + .../src/main/res/layout/activity_stream.xml | 2 +- .../res/layout/fragment_touchpad_only.xml | 22 +++++++++++++++---- 7 files changed, 46 insertions(+), 42 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 9c4ea34..2c474f0 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -165,7 +165,7 @@ data class ControllerState constructor( var leftY: Short = 0, var rightX: Short = 0, var rightY: Short = 0, - private var touchIdNext: UByte = 100U, + private var touchIdNext: UByte = 0U, var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), var gyroX: Float = 0.0f, var gyroY: Float = 0.0f, @@ -280,7 +280,8 @@ data class ControllerState constructor( .find { it.id < 0 } ?.also { it.id = touchIdNext.toByte() - Log.d("TouchId", "touch id next: $touchIdNext") + it.x = x + it.y = y touchIdNext = ((touchIdNext + 1U) and 0x7fU).toUByte() }?.id?.toUByte() diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index fd07ff4..b0248be 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -22,6 +22,7 @@ import com.metallic.chiaki.databinding.ActivityStreamBinding import com.metallic.chiaki.lib.ConnectInfo import com.metallic.chiaki.lib.ConnectVideoProfile import com.metallic.chiaki.session.* +import com.metallic.chiaki.touchcontrols.DefaultTouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment import io.reactivex.disposables.CompositeDisposable @@ -120,20 +121,14 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe override fun onAttachFragment(fragment: Fragment) { super.onAttachFragment(fragment) - when(fragment) + if(fragment is TouchControlsFragment) { - is TouchControlsFragment -> - { - fragment.controllerState - .subscribe { viewModel.input.touchControllerState = it } - .addTo(controlsDisposable) - fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled - } - is TouchpadOnlyFragment -> - { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } + fragment.controllerState + .subscribe { viewModel.input.touchControllerState = it } + .addTo(controlsDisposable) + fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled + if(fragment is TouchpadOnlyFragment) fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled - } } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index ad734ce..b3ab922 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -12,14 +12,14 @@ import androidx.lifecycle.Observer import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.lib.ControllerState import io.reactivex.Observable -import io.reactivex.Observable.combineLatest +import io.reactivex.rxkotlin.Observables.combineLatest import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject -class TouchControlsFragment : Fragment() +abstract class TouchControlsFragment : Fragment() { - private var ownControllerState = ControllerState() - private set(value) + protected var ownControllerState = ControllerState() + set(value) { val diff = field != value field = value @@ -27,17 +27,20 @@ class TouchControlsFragment : Fragment() ownControllerStateSubject.onNext(ownControllerState) } - private val ownControllerStateSubject: Subject + protected val ownControllerStateSubject: Subject = BehaviorSubject.create().also { it.onNext(ownControllerState) } // to delay attaching to the touchpadView until it's available - private val controllerStateProxy: Subject> - = BehaviorSubject.create>().also { it.onNext(ownControllerStateSubject) } + protected val controllerStateProxy: Subject> + = BehaviorSubject.create>().also { it.onNext(ownControllerStateSubject) } val controllerState: Observable get() = controllerStateProxy.flatMap { it } var onScreenControlsEnabled: LiveData? = null +} +class DefaultTouchControlsFragment : TouchControlsFragment() +{ private var _binding: FragmentControlsBinding? = null private val binding get() = _binding!! diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index ef96b84..788b6d5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -6,26 +6,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.metallic.chiaki.R -import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding import com.metallic.chiaki.lib.ControllerState +import io.reactivex.rxkotlin.Observables.combineLatest -class TouchpadOnlyFragment : Fragment() +class TouchpadOnlyFragment : TouchControlsFragment() { - private var controllerState = ControllerState() - private set(value) - { - val diff = field != value - field = value - if(diff) - controllerStateCallback?.let { it(value) } - } - - var controllerStateCallback: ((ControllerState) -> Unit)? = null var touchpadOnlyEnabled: LiveData? = null private var _binding: FragmentTouchpadOnlyBinding? = null @@ -34,6 +22,9 @@ class TouchpadOnlyFragment : Fragment() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = FragmentTouchpadOnlyBinding.inflate(inflater, container, false).let { _binding = it + controllerStateProxy.onNext( + combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b } + ) it.root } @@ -49,13 +40,12 @@ class TouchpadOnlyFragment : Fragment() } private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean -> - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = if(pressed) buttons or buttonMask else buttons and buttonMask.inv() - } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index 8c8f44f..b712c6e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -86,6 +86,7 @@ class TouchpadView @JvmOverloads constructor( private fun triggerStateChanged() { + invalidate() stateSubject.onNext(state) } } \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_stream.xml b/android/app/src/main/res/layout/activity_stream.xml index 99eea1b..6a7f9a3 100644 --- a/android/app/src/main/res/layout/activity_stream.xml +++ b/android/app/src/main/res/layout/activity_stream.xml @@ -28,7 +28,7 @@ diff --git a/android/app/src/main/res/layout/fragment_touchpad_only.xml b/android/app/src/main/res/layout/fragment_touchpad_only.xml index a42b78a..2da7a09 100644 --- a/android/app/src/main/res/layout/fragment_touchpad_only.xml +++ b/android/app/src/main/res/layout/fragment_touchpad_only.xml @@ -1,5 +1,6 @@ - + + \ No newline at end of file From bae081d5b319385ce6023bd4cc762f6c20f722e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 18:00:05 +0100 Subject: [PATCH 054/104] Trigger Touchpad Button from TouchpadView on Android --- .../touchcontrols/TouchControlsFragment.kt | 1 - .../touchcontrols/TouchpadOnlyFragment.kt | 4 - .../chiaki/touchcontrols/TouchpadView.kt | 89 +++++++++++++++++-- .../res/drawable/control_button_touchpad.xml | 12 --- .../control_button_touchpad_pressed.xml | 12 --- .../res/drawable/control_touchpad_pressed.xml | 12 +++ .../src/main/res/layout/fragment_controls.xml | 17 +--- .../res/layout/fragment_touchpad_only.xml | 15 +--- android/app/src/main/res/values/attrs.xml | 10 ++- 9 files changed, 105 insertions(+), 67 deletions(-) delete mode 100644 android/app/src/main/res/drawable/control_button_touchpad.xml delete mode 100644 android/app/src/main/res/drawable/control_button_touchpad_pressed.xml create mode 100644 android/app/src/main/res/drawable/control_touchpad_pressed.xml diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index b3ab922..b1ecfbe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -68,7 +68,6 @@ class DefaultTouchControlsFragment : TouchControlsFragment() binding.optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) binding.shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) - binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) binding.l2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { l2State = if(it) 255U else 0U } } binding.r2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { r2State = if(it) 255U else 0U } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index 788b6d5..8910ef7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -9,7 +9,6 @@ import android.view.ViewGroup import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding -import com.metallic.chiaki.lib.ControllerState import io.reactivex.rxkotlin.Observables.combineLatest class TouchpadOnlyFragment : TouchControlsFragment() @@ -31,9 +30,6 @@ class TouchpadOnlyFragment : TouchControlsFragment() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - touchpadOnlyEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index b712c6e..9dba03d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -13,24 +13,69 @@ import com.metallic.chiaki.lib.ControllerState import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject +import kotlin.math.max class TouchpadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + companion object + { + private const val BUTTON_PRESS_MAX_MOVE_DIST_DP = 32.0f + private const val SHORT_BUTTON_PRESS_DURATION_MS = 200L + private const val BUTTON_HOLD_DELAY_MS = 500L + } + + private val drawableIdle: Drawable? + private val drawablePressed: Drawable? + private val state: ControllerState = ControllerState() - private val pointerTouchIds = mutableMapOf() + + inner class Touch( + val stateId: UByte, + private val startX: Float, + private val startY: Float) + { + var lifted = false // will be true but touch still in list when only relevant for short touch + private var maxDist: Float = 0.0f + val moveInsignificant: Boolean get() = maxDist < BUTTON_PRESS_MAX_MOVE_DIST_DP + + fun onMove(x: Float, y: Float) + { + val d = (Vector(x, y) - Vector(startX, startY)).length / resources.displayMetrics.density + maxDist = max(d, maxDist) + } + + val startButtonHoldRunnable = Runnable { + if(!moveInsignificant || buttonHeld) + return@Runnable + state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD + buttonHeld = true + } + } + private val pointerTouches = mutableMapOf() private val stateSubject: Subject = BehaviorSubject.create().also { it.onNext(state) } val controllerState: Observable get() = stateSubject - private val drawable: Drawable? + private var shortPressingTouches = listOf() + private val shortButtonPressLiftRunnable = Runnable { + state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv() + shortPressingTouches.forEach { + state.stopTouch(it.stateId) + } + shortPressingTouches = listOf() + triggerStateChanged() + } + + private var buttonHeld = false init { context.theme.obtainStyledAttributes(attrs, R.styleable.TouchpadView, 0, 0).apply { - drawable = getDrawable(R.styleable.TouchpadView_drawable) + drawableIdle = getDrawable(R.styleable.TouchpadView_drawableIdle) + drawablePressed = getDrawable(R.styleable.TouchpadView_drawablePressed) recycle() } isClickable = true @@ -39,8 +84,9 @@ class TouchpadView @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - if(state.touches.find { it.id >= 0 } == null) + if(pointerTouches.values.find { !it.lifted } == null) return + val drawable = if(state.buttons and ControllerState.BUTTON_TOUCHPAD != 0U) drawablePressed else drawableIdle drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) drawable?.draw(canvas) } @@ -59,23 +105,40 @@ class TouchpadView @JvmOverloads constructor( { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { - pointerTouchIds[event.getPointerId(event.actionIndex)] = it + val touch = Touch(it, event.getX(event.actionIndex), event.getY(event.actionIndex)) + pointerTouches[event.getPointerId(event.actionIndex)] = touch + if(!buttonHeld) + postDelayed(touch.startButtonHoldRunnable, BUTTON_HOLD_DELAY_MS) triggerStateChanged() } } MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { - pointerTouchIds.remove(event.getPointerId(event.actionIndex))?.let { - state.stopTouch(it) + pointerTouches.remove(event.getPointerId(event.actionIndex))?.let { + removeCallbacks(it.startButtonHoldRunnable) + when + { + buttonHeld -> + { + buttonHeld = false + state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv() + state.stopTouch(it.stateId) + } + it.moveInsignificant -> triggerShortButtonPress(it) + else -> state.stopTouch(it.stateId) + } triggerStateChanged() } } MotionEvent.ACTION_MOVE -> { - val changed = pointerTouchIds.entries.fold(false) { acc, it -> + val changed = pointerTouches.entries.fold(false) { acc, it -> val index = event.findPointerIndex(it.key) if(index < 0) acc else - acc || state.setTouchPos(it.value, touchX(event, index), touchY(event, index)) + { + it.value.onMove(event.getX(event.actionIndex), event.getY(event.actionIndex)) + acc || state.setTouchPos(it.value.stateId, touchX(event, index), touchY(event, index)) + } } if(changed) triggerStateChanged() @@ -84,6 +147,14 @@ class TouchpadView @JvmOverloads constructor( return true } + private fun triggerShortButtonPress(touch: Touch) + { + shortPressingTouches = shortPressingTouches + listOf(touch) + removeCallbacks(shortButtonPressLiftRunnable) + state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD + postDelayed(shortButtonPressLiftRunnable, SHORT_BUTTON_PRESS_DURATION_MS) + } + private fun triggerStateChanged() { invalidate() diff --git a/android/app/src/main/res/drawable/control_button_touchpad.xml b/android/app/src/main/res/drawable/control_button_touchpad.xml deleted file mode 100644 index 40c0465..0000000 --- a/android/app/src/main/res/drawable/control_button_touchpad.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml b/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml deleted file mode 100644 index 8255860..0000000 --- a/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/control_touchpad_pressed.xml b/android/app/src/main/res/drawable/control_touchpad_pressed.xml new file mode 100644 index 0000000..b4f3f16 --- /dev/null +++ b/android/app/src/main/res/drawable/control_touchpad_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index ee2a4a8..244956a 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -51,8 +51,10 @@ android:id="@+id/touchpadView" android:layout_width="0dp" android:layout_height="0dp" - app:drawable="@drawable/control_touchpad" - app:layout_constraintTop_toBottomOf="@id/touchpadButtonView" + app:drawableIdle="@drawable/control_touchpad" + app:drawablePressed="@drawable/control_touchpad_pressed" + android:layout_marginTop="32dp" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_max="300dp" app:layout_constraintDimensionRatio="1920:942" app:layout_constraintRight_toRightOf="parent" @@ -169,17 +171,6 @@ app:layout_constraintBottom_toBottomOf="parent"/> - - - - + + + - - + + @@ -13,6 +16,7 @@ - + + \ No newline at end of file From 28f017d6408bad5e5b8a91c8f0020a86f631ad19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 18:23:20 +0100 Subject: [PATCH 055/104] Add Option to disable Motion on Android --- .../main/java/com/metallic/chiaki/common/Preferences.kt | 5 +++++ .../main/java/com/metallic/chiaki/session/StreamInput.kt | 5 +++-- .../com/metallic/chiaki/settings/SettingsFragment.kt | 2 ++ android/app/src/main/res/drawable/ic_motion.xml | 9 +++++++++ android/app/src/main/res/values/strings.xml | 3 +++ android/app/src/main/res/xml/preferences.xml | 6 ++++++ 6 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_motion.xml diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index d779c5a..2456fcf 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -78,6 +78,11 @@ class Preferences(context: Context) get() = sharedPreferences.getBoolean(rumbleEnabledKey, true) set(value) { sharedPreferences.edit().putBoolean(rumbleEnabledKey, value).apply() } + val motionEnabledKey get() = resources.getString(R.string.preferences_motion_enabled_key) + var motionEnabled + get() = sharedPreferences.getBoolean(motionEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(motionEnabledKey, value).apply() } + val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key) var logVerbose get() = sharedPreferences.getBoolean(logVerboseKey, false) diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt index 6fbfc31..822fe76 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt @@ -86,7 +86,7 @@ class StreamInput(val context: Context, val preferences: Preferences) override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} } - private val lifecycleObserver = object: LifecycleObserver { + private val motionLifecycleObserver = object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { @@ -111,7 +111,8 @@ class StreamInput(val context: Context, val preferences: Preferences) fun observe(lifecycleOwner: LifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + if(preferences.motionEnabled) + lifecycleOwner.lifecycle.addObserver(motionLifecycleObserver) } private fun controllerStateUpdated() diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 957d1d5..222d130 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -26,6 +26,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.logVerboseKey -> preferences.logVerbose preferences.swapCrossMoonKey -> preferences.swapCrossMoon preferences.rumbleEnabledKey -> preferences.rumbleEnabled + preferences.motionEnabledKey -> preferences.motionEnabled else -> defValue } @@ -36,6 +37,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.logVerboseKey -> preferences.logVerbose = value preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value + preferences.motionEnabledKey -> preferences.motionEnabled = value } } diff --git a/android/app/src/main/res/drawable/ic_motion.xml b/android/app/src/main/res/drawable/ic_motion.xml new file mode 100644 index 0000000..c5a4ea1 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_motion.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0f7161d..84a1a59 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -90,6 +90,8 @@ Swap face buttons if default mapping is incorrect (e.g. for 8BitDo controllers) Rumble Use phone vibration motor for rumble + Motion + Use device\'s motion sensors for controller motion Are you sure you want to delete the registered console %s with ID %s? Are you sure you want to delete the console entry for %s? Keep @@ -105,6 +107,7 @@ on_screen_controls_enabled touchpad_only_enabled rumble_enabled + motion_enabled log_verbose import_settings export_settings diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index e87afdb..0ec2c0d 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -25,6 +25,12 @@ app:summary="@string/preferences_rumble_enabled_summary" app:icon="@drawable/ic_rumble" /> + + Date: Fri, 15 Jan 2021 18:43:09 +0100 Subject: [PATCH 056/104] Add Touch Button Haptics to Android --- .../com/metallic/chiaki/common/Preferences.kt | 5 ++++ .../chiaki/settings/SettingsFragment.kt | 2 ++ .../chiaki/touchcontrols/ButtonHaptics.kt | 29 +++++++++++++++++++ .../chiaki/touchcontrols/ButtonView.kt | 4 +++ .../metallic/chiaki/touchcontrols/DPadView.kt | 4 +++ .../chiaki/touchcontrols/TouchpadView.kt | 5 ++++ .../main/res/drawable/ic_button_haptic.xml | 9 ++++++ android/app/src/main/res/values/strings.xml | 3 ++ android/app/src/main/res/xml/preferences.xml | 6 ++++ 9 files changed, 67 insertions(+) create mode 100644 android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt create mode 100644 android/app/src/main/res/drawable/ic_button_haptic.xml diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index 2456fcf..3c4f2f9 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -83,6 +83,11 @@ class Preferences(context: Context) get() = sharedPreferences.getBoolean(motionEnabledKey, true) set(value) { sharedPreferences.edit().putBoolean(motionEnabledKey, value).apply() } + val buttonHapticEnabledKey get() = resources.getString(R.string.preferences_button_haptic_enabled_key) + var buttonHapticEnabled + get() = sharedPreferences.getBoolean(buttonHapticEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(buttonHapticEnabledKey, value).apply() } + val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key) var logVerbose get() = sharedPreferences.getBoolean(logVerboseKey, false) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 222d130..f574d09 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -27,6 +27,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.swapCrossMoonKey -> preferences.swapCrossMoon preferences.rumbleEnabledKey -> preferences.rumbleEnabled preferences.motionEnabledKey -> preferences.motionEnabled + preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled else -> defValue } @@ -38,6 +39,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value preferences.motionEnabledKey -> preferences.motionEnabled = value + preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled = value } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt new file mode 100644 index 0000000..909eab4 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt @@ -0,0 +1,29 @@ +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import androidx.appcompat.app.AppCompatActivity +import com.metallic.chiaki.common.Preferences + +class ButtonHaptics(val context: Context) +{ + private val enabled = Preferences(context).buttonHapticEnabled + + fun trigger(harder: Boolean = false) + { + if(!enabled) + return + val vibrator = context.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + vibrator.vibrate( + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + VibrationEffect.createPredefined(if(harder) VibrationEffect.EFFECT_CLICK else VibrationEffect.EFFECT_TICK) + else + VibrationEffect.createOneShot(10, if(harder) 200 else 100) + ) + else + vibrator.vibrate(10) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index e82ffac..078f751 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -16,6 +16,8 @@ class ButtonView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + private val haptics = ButtonHaptics(context) + var buttonPressed = false private set(value) { @@ -23,6 +25,8 @@ class ButtonView @JvmOverloads constructor( field = value if(diff) { + if(value) + haptics.trigger() invalidate() buttonPressedCallback?.let { it(field) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index 0006546..9c359d8 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -16,6 +16,8 @@ class DPadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + private val haptics = ButtonHaptics(context) + enum class Direction { LEFT, RIGHT, @@ -113,6 +115,8 @@ class DPadView @JvmOverloads constructor( if(state != newState) { + if(newState != null) + haptics.trigger() state = newState invalidate() stateChangeCallback?.let { it(state) } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index 9dba03d..a7e7b85 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -26,6 +26,8 @@ class TouchpadView @JvmOverloads constructor( private const val BUTTON_HOLD_DELAY_MS = 500L } + private val haptics = ButtonHaptics(context) + private val drawableIdle: Drawable? private val drawablePressed: Drawable? @@ -49,8 +51,10 @@ class TouchpadView @JvmOverloads constructor( val startButtonHoldRunnable = Runnable { if(!moveInsignificant || buttonHeld) return@Runnable + haptics.trigger(true) state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD buttonHeld = true + triggerStateChanged() } } private val pointerTouches = mutableMapOf() @@ -105,6 +109,7 @@ class TouchpadView @JvmOverloads constructor( { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { + haptics.trigger() val touch = Touch(it, event.getX(event.actionIndex), event.getY(event.actionIndex)) pointerTouches[event.getPointerId(event.actionIndex)] = touch if(!buttonHeld) diff --git a/android/app/src/main/res/drawable/ic_button_haptic.xml b/android/app/src/main/res/drawable/ic_button_haptic.xml new file mode 100644 index 0000000..fea4c99 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_button_haptic.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 84a1a59..254042b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -92,6 +92,8 @@ Use phone vibration motor for rumble Motion Use device\'s motion sensors for controller motion + Touch Haptics + Use phone vibration motor for short haptic feedback on button touches Are you sure you want to delete the registered console %s with ID %s? Are you sure you want to delete the console entry for %s? Keep @@ -108,6 +110,7 @@ touchpad_only_enabled rumble_enabled motion_enabled + button_haptic_enabled log_verbose import_settings export_settings diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index 0ec2c0d..49989db 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -31,6 +31,12 @@ app:summary="@string/preferences_motion_enabled_summary" app:icon="@drawable/ic_motion" /> + + Date: Fri, 15 Jan 2021 18:50:49 +0100 Subject: [PATCH 057/104] Fix RegistActivity on Android --- .../src/main/java/com/metallic/chiaki/regist/RegistActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index a8d06f6..9190f9c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -41,7 +41,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity { super.onCreate(savedInstanceState) binding = ActivityRegistBinding.inflate(layoutInflater) - setContentView(R.layout.activity_regist) + setContentView(binding.root) handleReveal() viewModel = ViewModelProvider(this).get(RegistViewModel::class.java) From 35130b08b700591e9a279f53e8380e69ca4eca0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 19:16:54 +0100 Subject: [PATCH 058/104] Refine LR Touch on Android --- .../chiaki/touchcontrols/ButtonView.kt | 11 ++++++---- .../src/main/res/layout/fragment_controls.xml | 20 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index 078f751..66a9afe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -6,6 +6,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.util.Log import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -75,14 +76,16 @@ class ButtonView @JvmOverloads constructor( override fun onTouchEvent(event: MotionEvent): Boolean { - when(event.action) + when(event.actionMasked) { - MotionEvent.ACTION_DOWN -> { - if(bestFittingTouchView(event.x, event.y) != this) + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + if(bestFittingTouchView(event.getX(event.actionIndex), event.getY(event.actionIndex)) != this) return false buttonPressed = true } - MotionEvent.ACTION_UP -> buttonPressed = false + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + buttonPressed = false + } } return true } diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 244956a..7a6b3af 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -173,9 +173,12 @@ Date: Fri, 15 Jan 2021 21:48:59 +0100 Subject: [PATCH 059/104] Fix FindFFMPEG.cmake for ancient cmake --- cmake/FindFFMPEG.cmake | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake index 32e8ac0..bdb66aa 100644 --- a/cmake/FindFFMPEG.cmake +++ b/cmake/FindFFMPEG.cmake @@ -49,7 +49,11 @@ function (_ffmpeg_find component headername) # Try pkg-config first if(PKG_CONFIG_FOUND) - pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules(FFMPEG_${component} lib${component}) + else() + pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) + endif() if(FFMPEG_${component}_FOUND) if((TARGET PkgConfig::FFMPEG_${component}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) if(APPLE) @@ -69,6 +73,9 @@ function (_ffmpeg_find component headername) add_library(FFMPEG::${component} ALIAS PkgConfig::FFMPEG_${component}) else() add_library("FFMPEG::${component}" INTERFACE IMPORTED) + if(CMAKE_VERSION VERSION_LESS "3.6") + link_directories("${FFMPEG_${component}_LIBRARY_DIRS}") + endif() set_target_properties("FFMPEG::${component}" PROPERTIES INTERFACE_LINK_DIRECTORIES "${FFMPEG_${component}_LIBRARY_DIRS}" INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIRS}" @@ -224,10 +231,14 @@ foreach (_ffmpeg_component IN LISTS FFMPEG_FIND_COMPONENTS) list(APPEND _ffmpeg_required_vars "FFMPEG_${_ffmpeg_component}_LIBRARIES") else() - set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS - "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}") - set(FFMPEG_${_ffmpeg_component}_LIBRARIES - "${FFMPEG_${_ffmpeg_component}_LIBRARY}") + if(NOT FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS) + set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS + "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}") + endif() + if(NOT FFMPEG_${_ffmpeg_component}_LIBRARIES) + set(FFMPEG_${_ffmpeg_component}_LIBRARIES + "${FFMPEG_${_ffmpeg_component}_LIBRARY}") + endif() list(APPEND FFMPEG_INCLUDE_DIRS "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS}") list(APPEND FFMPEG_LIBRARIES From 505910bc5fc611183ce0831cf8f92936de99827e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 21:14:37 +0100 Subject: [PATCH 060/104] Enable Setsu in AppImage --- scripts/Dockerfile.xenial | 4 ++-- scripts/build-appimage.sh | 2 ++ scripts/build-ffmpeg.sh | 2 +- setsu/cmake/FindEvdev.cmake | 6 +++++- setsu/cmake/FindUdev.cmake | 6 +++++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/Dockerfile.xenial b/scripts/Dockerfile.xenial index b741028..dd92581 100644 --- a/scripts/Dockerfile.xenial +++ b/scripts/Dockerfile.xenial @@ -5,9 +5,9 @@ RUN apt-get update RUN apt-get install -y software-properties-common RUN add-apt-repository ppa:beineri/opt-qt-5.12.3-xenial RUN apt-get update -RUN apt-get -y install git g++ cmake ninja-build curl unzip python3-pip \ +RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ - libgl1-mesa-dev nasm libudev-dev libva-dev fuse + libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev CMD [] diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 7ba4acf..dd01ef4 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -18,6 +18,8 @@ cmake \ "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix;/opt/qt512" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ + -DCHIAKI_ENABLE_GUI=ON \ + -DCHIAKI_ENABLE_SETSU=ON \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ -DCMAKE_INSTALL_PREFIX=/usr \ .. diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index 715a87f..d00b962 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -9,6 +9,6 @@ TAG=n4.3.1 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 -./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 +./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 make -j4 || exit 1 make install || exit 1 diff --git a/setsu/cmake/FindEvdev.cmake b/setsu/cmake/FindEvdev.cmake index 5ea7dea..23f2515 100644 --- a/setsu/cmake/FindEvdev.cmake +++ b/setsu/cmake/FindEvdev.cmake @@ -5,7 +5,11 @@ set(_target "${_prefix}::libevdev") find_package(PkgConfig) if(PkgConfig_FOUND AND NOT TARGET ${_target}) - pkg_check_modules("${_prefix}" libevdev IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules("${_prefix}" libevdev) + else() + pkg_check_modules("${_prefix}" libevdev IMPORTED_TARGET) + endif() if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) add_library(${_target} ALIAS PkgConfig::${_prefix}) diff --git a/setsu/cmake/FindUdev.cmake b/setsu/cmake/FindUdev.cmake index fc1c81f..c9c8450 100644 --- a/setsu/cmake/FindUdev.cmake +++ b/setsu/cmake/FindUdev.cmake @@ -5,7 +5,11 @@ set(_target "${_prefix}::libudev") find_package(PkgConfig) if(PkgConfig_FOUND AND NOT TARGET ${_target}) - pkg_check_modules("${_prefix}" libudev IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules("${_prefix}" libudev) + else() + pkg_check_modules("${_prefix}" libudev IMPORTED_TARGET) + endif() if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) add_library(${_target} ALIAS PkgConfig::${_prefix}) From fcdc414692b33ecae1f30a19dc4cd81d4bd77121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 22:35:50 +0100 Subject: [PATCH 061/104] Version 2.1.0 --- CMakeLists.txt | 4 ++-- README.md | 18 ++++++++---------- android/app/build.gradle | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6beae8..02a1484 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,8 +31,8 @@ tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of s tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO) set(CHIAKI_VERSION_MAJOR 2) -set(CHIAKI_VERSION_MINOR 0) -set(CHIAKI_VERSION_PATCH 1) +set(CHIAKI_VERSION_MINOR 1) +set(CHIAKI_VERSION_PATCH 0) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/README.md b/README.md index 5129f3a..f306470 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,18 @@ for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potent ![Screenshot](assets/screenshot.png) +## Project Status + +As all relevant features are implemented, this project is considered to be finished and in maintenance mode only. +No major updates are planned and contributions are only accepted in special cases. + ## Installing You can either download a pre-built release or build Chiaki from source. ### Downloading a Release -Builds are provided for Linux, Android, macOS and Windows. +Builds are provided for Linux, Android, macOS, Nintendo Switch and Windows. You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). @@ -26,7 +31,7 @@ You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). * **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. -* **Switch**: Follow README specific [instructions](./switch/README.md) +* **Switch**: Download the `.nro` file and copy it into the `switch/` directory on your SD card. ### Building from Source @@ -39,7 +44,7 @@ cmake .. make ``` -For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md). +For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md) or [switch/](./switch/README.md) for Nintendo Switch. ## Usage @@ -63,13 +68,6 @@ Settings -> Remote Play -> Add Device, or on a PS5: Settings -> System -> Remote You can now double-click your Console in Chiaki's main window to start Remote Play. -## Joining the Community or Getting Help - -There are official groups for Chiaki on Telegram and IRC. They are bridged so you can join whichever you like: - -- **Telegram:** https://t.me/chiakitg -- **IRC:** #chiaki on irc.freenode.net - ## Acknowledgements This project has only been made possible because of the following Open Source projects: diff --git a/android/app/build.gradle b/android/app/build.gradle index 48bf54c..e87200c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 30 - versionCode 9 + versionCode 10 versionName chiakiVersion externalNativeBuild { cmake { From ac8a3a3a3c9428800de35a9a2bd00332da202b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 22:49:56 +0100 Subject: [PATCH 062/104] Update Flatpak to v2.1.0 --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 3a03107..992ca18 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://git.sr.ht/~thestr4ng3r/chiaki", - "tag": "v2.0.1", - "commit": "9e698dd7c4e4011ff6e136741abef5cf4b32527c" + "tag": "v2.1.0", + "commit": "fcdc414692b33ecae1f30a19dc4cd81d4bd77121" } ] } From 078e83ec70b3d008df8a4c1249e10457b46a1096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 10:11:46 +0100 Subject: [PATCH 063/104] Fix Regist on Switch --- switch/src/host.cpp | 1 + switch/src/settings.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 2f50e2f..39ac1b3 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -117,6 +117,7 @@ int Host::Register(int pin) throw Exception("Undefined PS4 system version (please run discover first)"); } + this->regist_info.pin = pin; this->regist_info.host = this->host_addr.c_str(); this->regist_info.broadcast = false; diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index 8cb5264..fb39861 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -7,9 +7,9 @@ Settings::Settings() { #if defined(__SWITCH__) - chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE ^ CHIAKI_LOG_DEBUG, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL & ~(CHIAKI_LOG_VERBOSE | CHIAKI_LOG_DEBUG), chiaki_log_cb_print, NULL); #else - chiaki_log_init(&this->log, CHIAKI_LOG_ALL, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL & ~CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); #endif } From 6df937a57ca63093e973af58de8776a6b4be607a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 10:17:49 +0100 Subject: [PATCH 064/104] Remove removed sdl2-static from Alpine --- .builds/common.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.builds/common.yml b/.builds/common.yml index 46b754f..6208cad 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -15,7 +15,6 @@ packages: - qt5-qtmultimedia-dev - ffmpeg-dev - sdl2-dev - - sdl2-static # this is gone on alpine edge so might be necessary to remove later - docker - fuse From 2257030adeb5c5a84380c78d34be4d783971663c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 10:13:29 +0100 Subject: [PATCH 065/104] Version 2.1.1 --- CMakeLists.txt | 2 +- android/app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 02a1484..611af18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submo set(CHIAKI_VERSION_MAJOR 2) set(CHIAKI_VERSION_MINOR 1) -set(CHIAKI_VERSION_PATCH 0) +set(CHIAKI_VERSION_PATCH 1) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/android/app/build.gradle b/android/app/build.gradle index e87200c..282b42c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 30 - versionCode 10 + versionCode 11 versionName chiakiVersion externalNativeBuild { cmake { From ae3ca1ac7a7cbdd6585a3c56ff72fdffba8809ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 14:02:17 +0100 Subject: [PATCH 066/104] Update Flatpak to v2.1.1 --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 992ca18..daab8a0 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://git.sr.ht/~thestr4ng3r/chiaki", - "tag": "v2.1.0", - "commit": "fcdc414692b33ecae1f30a19dc4cd81d4bd77121" + "tag": "v2.1.1", + "commit": "2257030adeb5c5a84380c78d34be4d783971663c" } ] } From a049ed43ec5e0316dee2edd718757ce643e3cd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 2 Feb 2021 10:59:26 +0100 Subject: [PATCH 067/104] Force-disable Lib Decoders on Android --- android/app/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index 282b42c..87b8761 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,6 +33,8 @@ android { "-DCHIAKI_ENABLE_GUI=OFF", "-DCHIAKI_ENABLE_SETSU=OFF", "-DCHIAKI_ENABLE_ANDROID=ON", + "-DCHIAKI_ENABLE_FFMPEG_DECODER=OFF", + "-DCHIAKI_ENABLE_PI_DECODER=OFF", "-DCHIAKI_LIB_ENABLE_OPUS=OFF", "-DCHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT=ON" } From f50b060795e4648eefc9efd431f0137adbd94a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 17:24:30 +0200 Subject: [PATCH 068/104] Fix AppVeyor --- .appveyor.yml | 6 +++--- scripts/appveyor-win.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 933f517..2731ccb 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -36,14 +36,14 @@ for: install: - git submodule update --init --recursive - sudo pip3 install protobuf - - brew install qt opus openssl@1.1 nasm sdl2 protobuf + - brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf - scripts/build-ffmpeg.sh build_script: - - export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt" + - export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt@5" - scripts/build-common.sh - cp -a build/gui/chiaki.app Chiaki.app - - /usr/local/opt/qt/bin/macdeployqt Chiaki.app -dmg + - /usr/local/opt/qt@5/bin/macdeployqt Chiaki.app -dmg artifacts: - path: Chiaki.dmg diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index f751200..585eb2c 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -26,7 +26,7 @@ ninja || exit 1 ninja install || exit 1 cd ../.. || exit 1 -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1d-dev.zip && 7z x openssl-1.1.1d-dev.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1l-dev.zip && 7z x openssl-1.1.1l-dev.zip || exit 1 wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 From 2b4a7426ff0ec96f89a311bed433826d59f879f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 14:57:21 +0200 Subject: [PATCH 069/104] Install CLI --- cli/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 83c37a9..83de1f9 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -15,3 +15,4 @@ endif() add_executable(chiaki-cli src/main.c) target_link_libraries(chiaki-cli chiaki-cli-lib) +install(TARGETS chiaki-cli) From a44000ea2b08a4bdfea397c2a3d493fd623dbcd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 15:11:56 +0200 Subject: [PATCH 070/104] Enable CLI in CI --- .builds/common.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.builds/common.yml b/.builds/common.yml index 6208cad..8c7d43d 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -17,6 +17,7 @@ packages: - sdl2-dev - docker - fuse + - argp-standalone artifacts: - chiaki.nro @@ -29,7 +30,7 @@ tasks: sudo service fuse start # Fuse for AppImages - local_build_and_test: | cd chiaki - cmake -Bbuild -GNinja + cmake -Bbuild -GNinja -DCHIAKI_ENABLE_CLI=ON -DCHIAKI_ENABLE_GUI=ON -DCHIAKI_CLI_ARGP_STANDALONE=ON ninja -C build build/test/chiaki-unit - appimage: | From 7870a28cdd5e42e36e10a55f27b05fc2bdb22562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 17:07:35 +0200 Subject: [PATCH 071/104] Fix Discovery in CLI --- cli/src/discover.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cli/src/discover.c b/cli/src/discover.c index 503b53a..cf9319f 100644 --- a/cli/src/discover.c +++ b/cli/src/discover.c @@ -142,13 +142,19 @@ CHIAKI_EXPORT int chiaki_cli_cmd_discover(ChiakiLog *log, int argc, char *argv[] return 1; } - ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); // TODO: IPv6, PS5, should probably use the service - ChiakiDiscoveryPacket packet; memset(&packet, 0, sizeof(packet)); packet.cmd = CHIAKI_DISCOVERY_CMD_SRCH; - - chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4; + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); + err = chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + if(err != CHIAKI_ERR_SUCCESS) + CHIAKI_LOGE(log, "Failed to send discovery packet for PS4: %s", chiaki_error_string(err)); + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5; + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS5); + err = chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + if(err != CHIAKI_ERR_SUCCESS) + CHIAKI_LOGE(log, "Failed to send discovery packet for PS5: %s", chiaki_error_string(err)); while(1) sleep(1); // TODO: wtf From 695da184733eace6bc97d6b4fef02f12483a533a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 17:13:26 +0200 Subject: [PATCH 072/104] Make CLI Wakeup work for PS5 --- cli/src/wakeup.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cli/src/wakeup.c b/cli/src/wakeup.c index cf5ab45..cea2792 100644 --- a/cli/src/wakeup.c +++ b/cli/src/wakeup.c @@ -11,10 +11,14 @@ static char doc[] = "Send a PS4 wakeup packet."; #define ARG_KEY_HOST 'h' #define ARG_KEY_REGISTKEY 'r' +#define ARG_KEY_PS4 '4' +#define ARG_KEY_PS5 '5' static struct argp_option options[] = { { "host", ARG_KEY_HOST, "Host", 0, "Host to send wakeup packet to", 0 }, - { "registkey", ARG_KEY_REGISTKEY, "RegistKey", 0, "PS4 registration key", 0 }, + { "registkey", ARG_KEY_REGISTKEY, "RegistKey", 0, "Remote Play registration key (plaintext)", 0 }, + { "ps4", ARG_KEY_PS4, NULL, 0, "PlayStation 4", 0 }, + { "ps5", ARG_KEY_PS5, NULL, 0, "PlayStation 5 (default)", 0 }, { 0 } }; @@ -22,6 +26,7 @@ typedef struct arguments { const char *host; const char *registkey; + bool ps5; } Arguments; static int parse_opt(int key, char *arg, struct argp_state *state) @@ -39,6 +44,12 @@ static int parse_opt(int key, char *arg, struct argp_state *state) case ARGP_KEY_ARG: argp_usage(state); break; + case ARG_KEY_PS4: + arguments->ps5 = false; + break; + case ARG_KEY_PS5: + arguments->ps5 = true; + break; default: return ARGP_ERR_UNKNOWN; } @@ -51,6 +62,7 @@ static struct argp argp = { options, parse_opt, 0, doc, 0, 0, 0 }; CHIAKI_EXPORT int chiaki_cli_cmd_wakeup(ChiakiLog *log, int argc, char *argv[]) { Arguments arguments = { 0 }; + arguments.ps5 = true; error_t argp_r = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, &arguments); if(argp_r != 0) return 1; @@ -73,5 +85,5 @@ CHIAKI_EXPORT int chiaki_cli_cmd_wakeup(ChiakiLog *log, int argc, char *argv[]) uint64_t credential = (uint64_t)strtoull(arguments.registkey, NULL, 16); - return chiaki_discovery_wakeup(log, NULL, arguments.host, credential, false); + return chiaki_discovery_wakeup(log, NULL, arguments.host, credential, arguments.ps5); } From 796a12845684afe5c4b194d4111bcdcf5b04aca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 18:19:46 +0200 Subject: [PATCH 073/104] Fix fec.c extension --- lib/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 36ac38c..3eb6e27 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -72,7 +72,7 @@ set(SOURCE_FILES src/controller.c src/takionsendbuffer.c src/time.c - src/fec + src/fec.c src/regist.c src/opusdecoder.c src/orientation.c) From 7a01ac0d411b0ec5b74b3a95726025ee88d450fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Dec 2021 11:46:47 +0100 Subject: [PATCH 074/104] Update BSD CI --- .builds/common.yml | 1 + .builds/freebsd.yml | 4 ++-- .builds/openbsd.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.builds/common.yml b/.builds/common.yml index 8c7d43d..b9ff1b7 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -9,6 +9,7 @@ packages: - ninja - protoc - py3-protobuf + - py3-setuptools - opus-dev - qt5-qtbase-dev - qt5-qtsvg-dev diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index caa9016..a105aec 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -1,5 +1,5 @@ -image: freebsd/latest +image: freebsd/13.x sources: - https://git.sr.ht/~thestr4ng3r/chiaki @@ -7,7 +7,7 @@ sources: packages: - cmake - protobuf - - py37-protobuf + - py38-protobuf - opus - qt5-core - qt5-qmake diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index b8785ef..2df5353 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,5 +1,5 @@ -image: openbsd/6.7 +image: openbsd/7.0 sources: - https://git.sr.ht/~thestr4ng3r/chiaki From dcd2e6af4a2403f68ad76774525f4d433bb43716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Dec 2021 11:47:32 +0100 Subject: [PATCH 075/104] Update nanopb to fix unaligned pointers on arm64 --- third-party/nanopb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third-party/nanopb b/third-party/nanopb index ae9901f..ab19ecb 160000 --- a/third-party/nanopb +++ b/third-party/nanopb @@ -1 +1 @@ -Subproject commit ae9901f2a31500e8fdc93fa9804d24851c58bb1e +Subproject commit ab19ecbe1b9f377ab4ee8e762bfe16c39068ad68 From ccecc67d74f35b0be41073c2a3ccd237ed955402 Mon Sep 17 00:00:00 2001 From: MiniMeOSc Date: Mon, 21 Mar 2022 22:43:08 +0100 Subject: [PATCH 076/104] Fix stream command not working for second or later host in configuration --- gui/src/main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gui/src/main.cpp b/gui/src/main.cpp index 010e744..a96f007 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -129,15 +129,21 @@ int real_main(int argc, char *argv[]) { if(args.length() < 3) parser.showHelp(1); + + bool found = false; for(const auto &temphost : settings.GetRegisteredHosts()) { if(temphost.GetServerNickname() == args[1]) { + found = true; morning = temphost.GetRPKey(); regist_key = temphost.GetRPRegistKey(); target = temphost.GetTarget(); break; } + } + if(!found) + { printf("No configuration found for '%s'\n", args[1].toLocal8Bit().constData()); return 1; } From aa3a3b8bbcfd1c45a0b10ec6d6d2aa4e3d8bb131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 26 Mar 2022 09:53:38 +0100 Subject: [PATCH 077/104] Update Windows Dependencies in CI --- scripts/appveyor-win.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index 585eb2c..1361114 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -26,7 +26,7 @@ ninja || exit 1 ninja install || exit 1 cd ../.. || exit 1 -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1l-dev.zip && 7z x openssl-1.1.1l-dev.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1n.zip && 7z x openssl-1.1.1n.zip || exit 1 wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 @@ -43,7 +43,7 @@ export PATH="$PWD/protoc/bin:$PATH" || exit 1 PYTHON="C:/Python37/python.exe" "$PYTHON" -m pip install protobuf || exit 1 -QT_PATH="C:/Qt/5.12/msvc2017_64" +QT_PATH="C:/Qt/5.15/msvc2019_64" COPY_DLLS="$PWD/openssl-1.1/x64/bin/libcrypto-1_1-x64.dll $PWD/openssl-1.1/x64/bin/libssl-1_1-x64.dll $SDL_ROOT/lib/x64/SDL2.dll" From 7d820bd4ab9fa3bc8614b4dff558b55a3db577a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 26 Mar 2022 13:44:50 +0100 Subject: [PATCH 078/104] Update and fix CI * appimage: switched to bionic * android: updated container for nanopb changes * switch: updated devkitpro and simpler cmake scripts --- .builds/android.yml | 2 +- .builds/common.yml | 2 +- .gitmodules | 2 +- cmake/switch.cmake | 26 ++------- scripts/Dockerfile.bionic | 16 ++++++ scripts/Dockerfile.xenial | 13 ----- scripts/kitware-archive-latest.asc | 64 +++++++++++++++++++++++ scripts/run-docker-build-appimage.sh | 4 +- scripts/switch/build.sh | 3 -- scripts/switch/run-docker-build-chiaki.sh | 2 +- switch/borealis | 2 +- 11 files changed, 90 insertions(+), 46 deletions(-) create mode 100644 scripts/Dockerfile.bionic delete mode 100644 scripts/Dockerfile.xenial create mode 100644 scripts/kitware-archive-latest.asc diff --git a/.builds/android.yml b/.builds/android.yml index 7b64ab4..5e4c259 100644 --- a/.builds/android.yml +++ b/.builds/android.yml @@ -22,7 +22,7 @@ tasks: sudo docker run \ -v /home/build:/home/build \ -u $(id -u):$(id -g) \ - thestr4ng3r/android:f064ea6 \ + thestr4ng3r/android:b2853cc \ /bin/bash -c "cd /home/build/chiaki/android && ./gradlew assembleRelease bundleRelease" cp chiaki/android/app/build/outputs/apk/release/app-release*.apk Chiaki.apk cp chiaki/android/app/build/outputs/bundle/release/app-release*.aab Chiaki.aab diff --git a/.builds/common.yml b/.builds/common.yml index b9ff1b7..4681f14 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -1,5 +1,5 @@ -image: alpine/latest +image: alpine/edge # on edge for https://gitlab.alpinelinux.org/alpine/aports/-/issues/13287 sources: - https://git.sr.ht/~thestr4ng3r/chiaki diff --git a/.gitmodules b/.gitmodules index ac87125..250f8fe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/google/oboe [submodule "switch/borealis"] path = switch/borealis - url = https://github.com/natinusala/borealis.git + url = https://git.sr.ht/~thestr4ng3r/borealis diff --git a/cmake/switch.cmake b/cmake/switch.cmake index 6046d67..7e600be 100644 --- a/cmake/switch.cmake +++ b/cmake/switch.cmake @@ -1,34 +1,15 @@ # Find DEVKITPRO set(DEVKITPRO "$ENV{DEVKITPRO}" CACHE PATH "Path to DevKitPro") -set(PORTLIBS_PREFIX "$ENV{PORTLIBS_PREFIX}" CACHE PATH "Path to portlibs inside DevKitPro") -if(NOT DEVKITPRO OR NOT PORTLIBS_PREFIX) - message(FATAL_ERROR "Please set DEVKITPRO & PORTLIBS_PREFIX env before calling cmake. https://devkitpro.org/wiki/Getting_Started") +if(NOT DEVKITPRO) + message(FATAL_ERROR "Please set DEVKITPRO env before calling cmake. https://devkitpro.org/wiki/Getting_Started") endif() # include devkitpro toolchain -include("${DEVKITPRO}/switch.cmake") +include("${DEVKITPRO}/cmake/Switch.cmake") set(NSWITCH TRUE) -# Enable gcc -g, to use -# /opt/devkitpro/devkitA64/bin/aarch64-none-elf-addr2line -e build_switch/switch/chiaki -f -p -C -a 0xCCB5C -# set(CMAKE_BUILD_TYPE Debug) -# set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "Shared libs not available" ) - -# FIXME rework this file to use the toolchain only -# https://github.com/diasurgical/devilutionX/pull/764 -set(ARCH "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -ftls-model=local-exec") -# set(CMAKE_C_FLAGS "-O2 -ffunction-sections ${ARCH}") -set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}") -# workaroud force -fPIE to avoid -# aarch64-none-elf/bin/ld: read-only segment has dynamic relocations -set(CMAKE_EXE_LINKER_FLAGS "-specs=${DEVKITPRO}/libnx/switch.specs ${ARCH} -fPIE -Wl,-Map,Output.map") - -# add portlibs to the list of include dir -include_directories("${PORTLIBS_PREFIX}/include") - # troubleshoot message(STATUS "CMAKE_FIND_ROOT_PATH = ${CMAKE_FIND_ROOT_PATH}") message(STATUS "PKG_CONFIG_EXECUTABLE = ${PKG_CONFIG_EXECUTABLE}") @@ -79,4 +60,3 @@ function(add_nro_target output_name target title author version icon romfs) endfunction() set(CMAKE_USE_SYSTEM_ENVIRONMENT_PATH OFF) -set(CMAKE_PREFIX_PATH "/") diff --git a/scripts/Dockerfile.bionic b/scripts/Dockerfile.bionic new file mode 100644 index 0000000..2548a2d --- /dev/null +++ b/scripts/Dockerfile.bionic @@ -0,0 +1,16 @@ + +FROM ubuntu:bionic + +RUN apt-get update +RUN apt-get install -y software-properties-common gpg wget +RUN add-apt-repository ppa:beineri/opt-qt-5.12.10-bionic +COPY kitware-archive-latest.asc /kitware-archive-latest.asc +RUN cat /kitware-archive-latest.asc | gpg --dearmor > /usr/share/keyrings/kitware-archive-keyring.gpg +RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' > /etc/apt/sources.list.d/kitware.list +RUN apt-get update +RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ + libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ + libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev + +CMD [] + diff --git a/scripts/Dockerfile.xenial b/scripts/Dockerfile.xenial deleted file mode 100644 index dd92581..0000000 --- a/scripts/Dockerfile.xenial +++ /dev/null @@ -1,13 +0,0 @@ - -FROM ubuntu:xenial - -RUN apt-get update -RUN apt-get install -y software-properties-common -RUN add-apt-repository ppa:beineri/opt-qt-5.12.3-xenial -RUN apt-get update -RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ - libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ - libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev - -CMD [] - diff --git a/scripts/kitware-archive-latest.asc b/scripts/kitware-archive-latest.asc new file mode 100644 index 0000000..6b3a357 --- /dev/null +++ b/scripts/kitware-archive-latest.asc @@ -0,0 +1,64 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=cUKW +-----END PGP PUBLIC KEY BLOCK----- diff --git a/scripts/run-docker-build-appimage.sh b/scripts/run-docker-build-appimage.sh index 931499c..b8625c8 100755 --- a/scripts/run-docker-build-appimage.sh +++ b/scripts/run-docker-build-appimage.sh @@ -3,13 +3,13 @@ set -xe cd "`dirname $(readlink -f ${0})`" -docker build -t chiaki-xenial . -f Dockerfile.xenial +docker build -t chiaki-bionic . -f Dockerfile.bionic cd .. docker run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ --device /dev/fuse \ --cap-add SYS_ADMIN \ - -t chiaki-xenial \ + -t chiaki-bionic \ /bin/bash -c "scripts/build-appimage.sh" diff --git a/scripts/switch/build.sh b/scripts/switch/build.sh index 1e03a2a..f04f4b2 100755 --- a/scripts/switch/build.sh +++ b/scripts/switch/build.sh @@ -5,10 +5,7 @@ set -xveo pipefail arg1=$1 build="./build" if [ "$arg1" != "linux" ]; then - # source /opt/devkitpro/switchvars.sh - # toolchain="${DEVKITPRO}/switch.cmake" toolchain="cmake/switch.cmake" - export PORTLIBS_PREFIX="$(${DEVKITPRO}/portlibs_prefix.sh switch)" build="./build_switch" fi diff --git a/scripts/switch/run-docker-build-chiaki.sh b/scripts/switch/run-docker-build-chiaki.sh index c81c788..c52933b 100755 --- a/scripts/switch/run-docker-build-chiaki.sh +++ b/scripts/switch/run-docker-build-chiaki.sh @@ -6,6 +6,6 @@ docker run \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ -t \ - thestr4ng3r/chiaki-build-switch \ + thestr4ng3r/chiaki-build-switch:35829cc \ -c "scripts/switch/build.sh" diff --git a/switch/borealis b/switch/borealis index cbdc1b6..eae1371 160000 --- a/switch/borealis +++ b/switch/borealis @@ -1 +1 @@ -Subproject commit cbdc1b65314d1eeb2799deae5cf6f113d6d67b46 +Subproject commit eae1371831d6cebf11b8ebd4c611069bccc6fb9b From 420809b24ef7bb8510a13d9a4c02ac260a9841a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jun 2022 17:10:19 +0200 Subject: [PATCH 079/104] Add option to fetch mbedtls with cmake --- CMakeLists.txt | 12 ++++++++++++ lib/CMakeLists.txt | 14 +++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 611af18..e9cc357 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ endif() tri_option(CHIAKI_ENABLE_FFMPEG_DECODER "Enable FFMPEG video decoder" ${CHIAKI_FFMPEG_DEFAULT}) tri_option(CHIAKI_ENABLE_PI_DECODER "Enable Raspberry Pi-specific video decoder (requires libraspberrypi0 and libraspberrypi-doc)" AUTO) option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF) +option(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT "Fetch Mbed TLS instead of using system-provided libs" OFF) option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON) option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) @@ -89,6 +90,17 @@ endif() if(CHIAKI_LIB_ENABLE_MBEDTLS) add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS) + if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + include(FetchContent) + set(ENABLE_TESTING OFF) + set(USE_SHARED_MBEDTLS_LIBRARY OFF) + FetchContent_Declare( + mbedtls + GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git + GIT_TAG 8b3f26a5ac38d4fdccbc5c5366229f3e01dafcc0 # v2.28.0 + ) + FetchContent_MakeAvailable(mbedtls) + endif() endif() if(CHIAKI_ENABLE_FFMPEG_DECODER) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 3eb6e27..cbfd6b0 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -119,11 +119,15 @@ find_package(Threads REQUIRED) target_link_libraries(chiaki-lib Threads::Threads) if(CHIAKI_LIB_ENABLE_MBEDTLS) - # provided by mbedtls-static (mbedtls-devel) - find_library(MBEDTLS mbedtls) - find_library(MBEDX509 mbedx509) - find_library(MBEDCRYPTO mbedcrypto) - target_link_libraries(chiaki-lib ${MBEDTLS} ${MBEDX509} ${MBEDCRYPTO}) + if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + target_link_libraries(chiaki-lib mbedtls mbedx509 mbedcrypto) + else() + # provided by mbedtls-static (mbedtls-devel) + find_library(MBEDTLS mbedtls) + find_library(MBEDX509 mbedx509) + find_library(MBEDCRYPTO mbedcrypto) + target_link_libraries(chiaki-lib ${MBEDTLS} ${MBEDX509} ${MBEDCRYPTO}) + endif() elseif(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT) target_link_libraries(chiaki-lib OpenSSL_Crypto) else() From b4f051395fdd781e826fc5eac75b6de45e1917b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jun 2022 17:50:18 +0200 Subject: [PATCH 080/104] Reduce targets in fetched mbedtls and show progress --- CMakeLists.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9cc357..3a552a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,13 +91,16 @@ endif() if(CHIAKI_LIB_ENABLE_MBEDTLS) add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS) if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + set(FETCHCONTENT_QUIET CACHE BOOL FALSE) include(FetchContent) - set(ENABLE_TESTING OFF) - set(USE_SHARED_MBEDTLS_LIBRARY OFF) + set(ENABLE_TESTING CACHE INTERNAL OFF) + set(ENABLE_PROGRAMS CACHE INTERNAL OFF) + set(USE_SHARED_MBEDTLS_LIBRARY CACHE INTERNAL OFF) FetchContent_Declare( mbedtls GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git GIT_TAG 8b3f26a5ac38d4fdccbc5c5366229f3e01dafcc0 # v2.28.0 + GIT_PROGRESS TRUE ) FetchContent_MakeAvailable(mbedtls) endif() From b790fb3fb5670fde67ab63571939ab03b529b7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jun 2022 18:26:42 +0200 Subject: [PATCH 081/104] Set PATH to find protoc for nanopb generator --- lib/protobuf/CMakeLists.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/protobuf/CMakeLists.txt b/lib/protobuf/CMakeLists.txt index 6efa342..43fde68 100644 --- a/lib/protobuf/CMakeLists.txt +++ b/lib/protobuf/CMakeLists.txt @@ -11,8 +11,16 @@ add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" set(SOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/takion.pb.c") set(HEADER_FILES "${CMAKE_CURRENT_BINARY_DIR}/takion.pb.h") +if(UNIX AND IS_ABSOLUTE "${PROTOC}") + # make sure protoc is in PATH when invoking the generator below, which needs it. + get_filename_component(PROTOC_PATH "${PROTOC}" DIRECTORY) + set(GEN_PREFIX "${CMAKE_COMMAND}" -E env "PATH=${PROTOC_PATH}:$ENV{PATH}") +else() + set(GEN_PREFIX "") +endif() + add_custom_command(OUTPUT ${SOURCE_FILES} ${HEADER_FILES} - COMMAND "${PYTHON_EXECUTABLE}" "${NANOPB_GENERATOR_PY}" "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" + COMMAND ${GEN_PREFIX} "${PYTHON_EXECUTABLE}" "${NANOPB_GENERATOR_PY}" "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" MAIN_DEPENDENCY "${CMAKE_CURRENT_BINARY_DIR}/takion.pb") set(CHIAKI_LIB_PROTO_SOURCE_FILES "${SOURCE_FILES}" PARENT_SCOPE) From 4164255ef9ce8b74c908b4d24973368c78c997e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Sep 2022 14:00:59 +0200 Subject: [PATCH 082/104] Update dependencies and fix CI * nanopb 0.4.6.4 with PB_C99_STATIC_ASSERT to avoid depending on C11 * OpenSSL 1.1.1q on windows * Switched from docker to podman in CI * Appdir in appimage build container is now in /build/appdir to fix "Invalid cross-device link" errors in linuxdeploy when appdir is in mount * Use python protobuf==3.19.5 in bionic image, which still supports python 3.6, and on windows where nanopb otherwise has issues * Purge nanopb_pb2.py to force regeneration of it for possibly different python protobuf versions in subsequent builds * FreeBSD needs py39 packages now --- .appveyor.yml | 1 + .builds/common.yml | 24 ++++++++++++------- .builds/freebsd.yml | 2 +- scripts/appveyor-win.sh | 4 ++-- scripts/build-appimage.sh | 13 +++++++--- scripts/build-common.sh | 3 +++ ...pimage.sh => run-podman-build-appimage.sh} | 6 ++--- ...llseye.sh => run-podman-build-bullseye.sh} | 5 ++-- scripts/switch/build.sh | 3 +++ ...-chiaki.sh => push-podman-build-chiaki.sh} | 0 ...d-chiaki.sh => run-podman-build-chiaki.sh} | 2 +- switch/README.md | 4 ++-- third-party/CMakeLists.txt | 2 ++ third-party/nanopb | 2 +- 14 files changed, 48 insertions(+), 23 deletions(-) rename scripts/{run-docker-build-appimage.sh => run-podman-build-appimage.sh} (58%) rename scripts/{run-docker-build-bullseye.sh => run-podman-build-bullseye.sh} (61%) rename scripts/switch/{push-docker-build-chiaki.sh => push-podman-build-chiaki.sh} (100%) rename scripts/switch/{run-docker-build-chiaki.sh => run-podman-build-chiaki.sh} (93%) diff --git a/.appveyor.yml b/.appveyor.yml index 2731ccb..7dc4799 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,6 +5,7 @@ image: branches: only: - master + - develop - /^v\d.*$/ - /^deploy-test(-.*)?$/ diff --git a/.builds/common.yml b/.builds/common.yml index 4681f14..0bce7cb 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -16,8 +16,9 @@ packages: - qt5-qtmultimedia-dev - ffmpeg-dev - sdl2-dev - - docker + - podman - fuse + - udev - argp-standalone artifacts: @@ -25,10 +26,17 @@ artifacts: - Chiaki.AppImage tasks: - - start_docker: | - sudo service docker start - sudo chmod +s /usr/bin/docker # Yes, I know what I am doing - sudo service fuse start # Fuse for AppImages + - setup_podman: | + sudo rc-service udev start + sudo rc-service cgroups start + sudo rc-service fuse start # Fuse for AppImages + echo build:100000:65536 | sudo tee /etc/subuid + echo build:100000:65536 | sudo tee /etc/subgid + # https://www.kernel.org/doc/Documentation/networking/tuntap.txt + # for slirp4netns + sudo mkdir -p /dev/net + sudo mknod /dev/net/tun c 10 200 + sudo chmod 0666 /dev/net/tun - local_build_and_test: | cd chiaki cmake -Bbuild -GNinja -DCHIAKI_ENABLE_CLI=ON -DCHIAKI_ENABLE_GUI=ON -DCHIAKI_CLI_ARGP_STANDALONE=ON @@ -36,12 +44,12 @@ tasks: build/test/chiaki-unit - appimage: | cd chiaki - scripts/run-docker-build-appimage.sh + scripts/run-podman-build-appimage.sh cp appimage/Chiaki.AppImage ../Chiaki.AppImage - switch: | cd chiaki - scripts/switch/run-docker-build-chiaki.sh + scripts/switch/run-podman-build-chiaki.sh cp build_switch/switch/chiaki.nro ../chiaki.nro - bullseye: | cd chiaki - scripts/run-docker-build-bullseye.sh + scripts/run-podman-build-bullseye.sh diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index a105aec..c62a695 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -7,7 +7,7 @@ sources: packages: - cmake - protobuf - - py38-protobuf + - py39-protobuf - opus - qt5-core - qt5-qmake diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index 1361114..3951d84 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -26,7 +26,7 @@ ninja || exit 1 ninja install || exit 1 cd ../.. || exit 1 -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1n.zip && 7z x openssl-1.1.1n.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip || exit 1 wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 @@ -41,7 +41,7 @@ cd .. || exit 1 export PATH="$PWD/protoc/bin:$PATH" || exit 1 PYTHON="C:/Python37/python.exe" -"$PYTHON" -m pip install protobuf || exit 1 +"$PYTHON" -m pip install protobuf==3.19.5 || exit 1 QT_PATH="C:/Qt/5.15/msvc2019_64" diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index dd01ef4..83903fd 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -2,9 +2,12 @@ set -xe +# sometimes there are errors in linuxdeploy in docker/podman when the appdir is on a mount +appdir=${1:-`pwd`/appimage/appdir} + mkdir appimage -pip3 install --user protobuf +pip3 install --user protobuf==3.19.5 # need support for python 3.6 for running on bionic scripts/fetch-protoc.sh appimage export PATH="`pwd`/appimage/protoc/bin:$PATH" scripts/build-ffmpeg.sh appimage @@ -24,10 +27,14 @@ cmake \ -DCMAKE_INSTALL_PREFIX=/usr \ .. cd .. + +# purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version +rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + ninja -C build_appimage build_appimage/test/chiaki-unit -DESTDIR=`pwd`/appimage/appdir ninja -C build_appimage install +DESTDIR="${appdir}" ninja -C build_appimage install cd appimage curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage @@ -41,5 +48,5 @@ set -e export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" export EXTRA_QT_PLUGINS=opengl -./linuxdeploy-x86_64.AppImage --appdir=appdir -e appdir/usr/bin/chiaki -d appdir/usr/share/applications/chiaki.desktop --plugin qt --output appimage +./linuxdeploy-x86_64.AppImage --appdir="${appdir}" -e "${appdir}/usr/bin/chiaki" -d "${appdir}/usr/share/applications/chiaki.desktop" --plugin qt --output appimage mv Chiaki-*-x86_64.AppImage Chiaki.AppImage diff --git a/scripts/build-common.sh b/scripts/build-common.sh index 0eb87b9..dd4be2c 100755 --- a/scripts/build-common.sh +++ b/scripts/build-common.sh @@ -1,5 +1,8 @@ #!/bin/bash +# purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version +rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + mkdir build && cd build || exit 1 cmake \ -DCMAKE_BUILD_TYPE=Release \ diff --git a/scripts/run-docker-build-appimage.sh b/scripts/run-podman-build-appimage.sh similarity index 58% rename from scripts/run-docker-build-appimage.sh rename to scripts/run-podman-build-appimage.sh index b8625c8..5dea4b4 100755 --- a/scripts/run-docker-build-appimage.sh +++ b/scripts/run-podman-build-appimage.sh @@ -3,13 +3,13 @@ set -xe cd "`dirname $(readlink -f ${0})`" -docker build -t chiaki-bionic . -f Dockerfile.bionic +podman build -t chiaki-bionic . -f Dockerfile.bionic cd .. -docker run --rm \ +podman run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ --device /dev/fuse \ --cap-add SYS_ADMIN \ -t chiaki-bionic \ - /bin/bash -c "scripts/build-appimage.sh" + /bin/bash -c "scripts/build-appimage.sh /build/appdir" diff --git a/scripts/run-docker-build-bullseye.sh b/scripts/run-podman-build-bullseye.sh similarity index 61% rename from scripts/run-docker-build-bullseye.sh rename to scripts/run-podman-build-bullseye.sh index fd19bb1..ad5c6d1 100755 --- a/scripts/run-docker-build-bullseye.sh +++ b/scripts/run-podman-build-bullseye.sh @@ -3,10 +3,11 @@ set -xe cd "`dirname $(readlink -f ${0})`" -docker build -t chiaki-bullseye . -f Dockerfile.bullseye +podman build -t chiaki-bullseye . -f Dockerfile.bullseye cd .. -docker run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " +podman run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " cd /build && + rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py && mkdir build_bullseye && cmake -Bbuild_bullseye -GNinja -DCHIAKI_ENABLE_SETSU=ON -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && ninja -C build_bullseye && diff --git a/scripts/switch/build.sh b/scripts/switch/build.sh index f04f4b2..37b825d 100755 --- a/scripts/switch/build.sh +++ b/scripts/switch/build.sh @@ -16,6 +16,9 @@ build_chiaki (){ pushd "${BASEDIR}" #rm -rf ./build + # purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version + rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + cmake -B "${build}" \ -GNinja \ -DCMAKE_TOOLCHAIN_FILE=${toolchain} \ diff --git a/scripts/switch/push-docker-build-chiaki.sh b/scripts/switch/push-podman-build-chiaki.sh similarity index 100% rename from scripts/switch/push-docker-build-chiaki.sh rename to scripts/switch/push-podman-build-chiaki.sh diff --git a/scripts/switch/run-docker-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh similarity index 93% rename from scripts/switch/run-docker-build-chiaki.sh rename to scripts/switch/run-podman-build-chiaki.sh index c52933b..520188b 100755 --- a/scripts/switch/run-docker-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -2,7 +2,7 @@ cd "`dirname $(readlink -f ${0})`/../.." -docker run \ +podman run \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ -t \ diff --git a/switch/README.md b/switch/README.md index f5fbe68..2307fa4 100644 --- a/switch/README.md +++ b/switch/README.md @@ -7,7 +7,7 @@ but the easiest way is to use the following container. Build Project ------------- ``` -bash scripts/switch/run-docker-build-chiaki.sh +bash scripts/switch/run-podman-build-chiaki.sh ``` tools @@ -15,7 +15,7 @@ tools Push to homebrew Netloader ``` # where X.X.X.X is the IP of your switch -bash scripts/switch/push-docker-build-chiaki.sh -a 192.168.0.200 +bash scripts/switch/push-podman-build-chiaki.sh -a 192.168.0.200 ``` Troubleshoot diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index b1f0b46..f6ebc40 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -4,11 +4,13 @@ if(NOT CHIAKI_USE_SYSTEM_NANOPB) # nanopb ################## + add_definitions(-DPB_C99_STATIC_ASSERT) # Fix PB_STATIC_ASSERT on msvc without using C11 for now add_subdirectory(nanopb EXCLUDE_FROM_ALL) set(NANOPB_GENERATOR_PY "${CMAKE_CURRENT_SOURCE_DIR}/nanopb/generator/nanopb_generator.py" PARENT_SCOPE) add_library(nanopb INTERFACE) target_link_libraries(nanopb INTERFACE protobuf-nanopb-static) target_include_directories(nanopb INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/nanopb") + target_compile_definitions(nanopb INTERFACE -DPB_C99_STATIC_ASSERT) # see above add_library(Nanopb::nanopb ALIAS nanopb) endif() diff --git a/third-party/nanopb b/third-party/nanopb index ab19ecb..afc499f 160000 --- a/third-party/nanopb +++ b/third-party/nanopb @@ -1 +1 @@ -Subproject commit ab19ecbe1b9f377ab4ee8e762bfe16c39068ad68 +Subproject commit afc499f9a410fc9bbf6c9c48cdd8d8b199d49eb4 From e00f5fae9df25cb263c380202446caa4b40a8d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 24 Sep 2022 19:47:31 +0200 Subject: [PATCH 083/104] Update macOS icon in Big Sur style --- assets/chiaki_macos.svg | 266 +++++++++++++++++++++++++++++++++ assets/chiaki_macos_simple.svg | 102 +++++++++++++ gui/chiaki.icns | Bin 164978 -> 363706 bytes gui/res/chiaki_macos.svg | 102 +++++++++++++ gui/res/resources.qrc | 1 + gui/src/main.cpp | 4 + 6 files changed, 475 insertions(+) create mode 100644 assets/chiaki_macos.svg create mode 100644 assets/chiaki_macos_simple.svg create mode 100644 gui/res/chiaki_macos.svg diff --git a/assets/chiaki_macos.svg b/assets/chiaki_macos.svg new file mode 100644 index 0000000..a4ce8f4 --- /dev/null +++ b/assets/chiaki_macos.svg @@ -0,0 +1,266 @@ + + + + diff --git a/assets/chiaki_macos_simple.svg b/assets/chiaki_macos_simple.svg new file mode 100644 index 0000000..7f1359d --- /dev/null +++ b/assets/chiaki_macos_simple.svg @@ -0,0 +1,102 @@ + + + + diff --git a/gui/chiaki.icns b/gui/chiaki.icns index 13613ffeacadacdc69503fa828f7386ed8b71e4e..e51fa65a4688c29e0553810e9a27b44fa3316c5d 100644 GIT binary patch literal 363706 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
    lkzjiz%8l=zbwu(2oYHODCe^+(G#lFp;8 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ⅅ%oZL@`l;+qKUJKSbKxGVUuRji%oDHeUp|dY`U8#2Jf& zDh{Xi=wE;f1}=Y1%%$s!(`W~EG476K&l>`WNvq~VJ{$_(9aKx z31QW9`Q>dwVB*$%0j0o;x18#Hoix#uLO6>LKv8lR7TqV}y)aJqo9MxTOK_(8vZcjV zDD7Gh>*+vn$Un72aD{|8B5a3tXGVYKK1=`Y@LmpY(Exs8lciQiFe5p`+wPL8?2s|o z!2Vdb2uqLU)7)2v0|$B+#K$})Se1M)Df>g4A2Ha-DS~ORY}BV9mw{<_)mzUK>_qVK zUNy3+t5DDIN*C;O_yGx0cmEQ=Cel5b@aJltfhXeL3Kz&A)mDY^zNb9GGBrCq+ z^rw_o`KK-6tJ(GliO5r2aSp#G;el3y(^KZ(!5X38gEvEbO{nZsGcIjY;CaVIWj;V| zQi|V2ekFs@v>l}z$?jSliA*=I(B9yED8E{H2^%2)33Q0^#0ju0W@B;BNZj1#=eE^4 zce{12+RFSIJqB38)hD0=vMv{CpJ;2X^=;Lb9SnClhEJnTl7a7P=+1qlMLW?^dIA9V zyHY@#WUg2ydV$D%wsz_cE~`#e7j3bmL&H-2orqfDlS&s@LD(sHGt&y63IQ@2E=p)m zPSj2rEYM=-i5$%ArP=c?p#46SydBja8F)F!J7qD>3HbGxh8fzZD1t$pDqIpY>otkHqY5lGtLnEP;!M?Vm&O zWo4Be$9w&x%?=(BC~mHa9?;beD&Vr$43fCGNoBI7nTbW5-RRCo{RhMr5~2?|b}Mo@ zq|Beo+;6TxKDKRf4RL?H@T1^iYe^sLB*fBgod4Hv_k38&IHJ^{1QQz>RVNe-? z4^A8Z21lLuzn6SzpuZRO=Tk{f#P_nN=ZT9&b2OP`V@=LwQAUf=v@}3i6#$GtO(|(H zFU3_B3I+Q=(S`kP@MB@%ZeW^{M}Xz&phN3V$A^XWArmLEuBD2tGWsTNr#S=Dy*63( zn9i%@h4(M>FJPaM4*+lzjNSar`)7d`NB67~M@bpaD+C6rn8O=8| z_HgI$L>BtaUi*+Y?QMN==&s?^HJoCKUU)g9$ox|doZRd$MpC{IJ=%?^y1qct1h-X7 z*P+aNJ1~Y{QG~}L)qQn|A7)=tgByG8?Jxou$8O5kNBmdRPw$-K+I|ofEG66uxvHf< z@}Zl5iJ9qy7J+sV_zEQ9zVjql z>5JH@rcWY?*xm-OQZ{u3g% zoLC0R_4pZ)kQwb;t${!|F1S#)pdM(S0iN-OWid{1!l@;l=J_YzS2MY{$lm>p;{(bu zbojw|Q}7ImDj#qAq_}inr?YK9j&$lOJJ*N-4=QkcBoOjuyw^Lvj_j95>=UqtN*fWd zuZHP8q(9`l(mnfT(Kvd|S|u+tBzX4{U;FLJ`iV#BHTBVk$DU}FvWo7dmBvmZGiUO| zcyK6rwncy}Mx5*RfvfwsRr3IIFtwFrVpW|{#-oZdE6ym3fstiPhBJ3RxlESNffF8x ze%lj13r&nanX{i_bmAK8Gs8C=`Y0>Q;t5O*oc)>GZF}fMb7~a3G)eiD#g_GrT$`nO z+A*LkXA!id2;7FBCh>4LXEE5UKCnbP)JqRVT(fGO@#0ZBXKc|t)cSuVK*VY1`2cPvy%mB%&%3hV@beGpdYfSyq2IG3>C5y^}9#1 zhw1l`CW%;lLR2FGx;T~g>E5PCYd1+AU;b!>wr7fv?kWx zpY8S>e}D@e+Tlb*)AqvtURTpT+MptIqT!(>1=Cg5CiGRkO^*KNDkt7Ui-7ayYKV^(l56_nU z5O-CZbAjFl~(~kjH7(5PQuarRwHJaBb8VT|I9ed^oiVa}peN1pP2(5KTfrnV+I; zs-i^mA3h)8BH68?8O=N8+LB#0(nOl_b5kDw(JnvU+WAgb(fF9#Jo2tb{%DIgoz_#J zxFA*+Z3hQkF;8a?@%_w~Maqwvy>6i!Njm9)IjLn|tpR_}DgLh$Bph>P25&G5r+T&PB zug|a#8e)5__>6Lc4e^VJ1(-R2xJGe&_d6cBT{s=BTTsP;tZ&L3m1T^Hh=}lZzy32v z46!>asn~~q@sHQL+OA5D>8CX;tChz`5R>kb{q7kfVdray52M7dib!uI5D96xRCv;D zo%egdj$T}J%RIVT2{jYZch<-V;^8rKV@T*aMK)yvW?q1Mrzc1G`)ts%Lw!Ou%u$EZ zAECncwjV^AL+C%sHnzVAOa?36W{_r06gT74SvGE+$as!|nQHFjr)DcE6!V*v{7-(h zo0W>VEL@wEbF<5wYOSGCcir`m`IO}<^zCFz<9(SKR`rmJv-;m21?jRw?;#dxK*}C3 z;b^A5I%CFY~PKfn|P=dF?KT&hRO|WUeD#hOZSpiSaY`jJ(09k(IeC$GRM}+B;={ z*OUFsao|+Zo6t^?f5;*ed8-8OSetQbE4%qbSpGQO?j0hso6wT3ARXYx6+l%NT%IHL zEtXgn6*}mMfefqL-Z$YH9sc+-hEppZ@gZk$53F-<{HNN#idxH^|I55PtyIs$!-T^C)c%CP5R6arSaK zyE}Y#=5|_%jdkq|_IO2K-3odVj>Pak_0NBjYY;lhz!G@3cEp> zw;a=5)Zw@HVj2x`?kg_UaS|+_Se4P7tQl;mBTs}Y`hU?%`jU?)?k7|Q!1Oo`yE=s` za~JR1#M9de!|=Dc^C0)miP~D&VD*rpccfr<(<`)i`Qw| zJY$3it(RZ_VjTM&K$LE^(wDarb9ymBeR)SK0LcJXNtI}D)pWYTN?TxBzFukewWfG8 z_5NZ+{T?;uT)jD_^}r)Gg+qiSXVCI-Y1XPi6B+hBX&e*hIyao}Z0fyM$C!G&79D32JUP^9#_V`@o(_kS{FHb~^tSBM)JSNqSXAw5teT zH(NI+G?fjwM4!fok&>ZLV`dI1yw=k^`fR~JNmwI~VX`dFrY1$9)ktYCo~=##Wo^-v z+93LCXH4xCJvcs&p`I}T*<(OC$vmfyM~hA}=jwCEKpe%}ji2fH@cbru>Z$lowmudA{*m2Qv=t5-sLHHw6>Zv;p~uj(YY17 zJRaRlAOE&>!N24*u=Xwyrt7?HCB1=t>ZG&u6hF2Wh$-;Ry#+V&f_vdc40W=pcHRjx z%fvDPSI_h*;5CeDXHV$zb*OzN3nnKcF&&=nymAgQteKJ#J0xJ+2xTfrFs9X$Nqa&H zmRGd1Jm=uVrT3^>g%1{!1$h%WNWtr1yD$Z4WwXcW) zO(5dVYPi=A`6c4E+y06B+a^R!eeLoYMBtg$^2J)NA`~ zKQJ1LJ!jO-Pz^sVbLM=;-lfwRr!YfC7)bUn*-duo=d|K6dAH(I%x3Q@JzHojR^|J* z)L~PmN3ZUSmqz*{+1*~EVqt?>A~E_>%4gCcHl_OlgU}WT^QX=zNteE*&}8ZadW_N; z%}WuKtbeJ0$|HZU@DXxgF&3d>(8)MfgU=vdQnQ(f(>;uI=}z|mE8!eE*#m86otxq-|F*Fa3(9`icf zNCqEMv%Y-4;9^r15%9!3qKP|62Ettl!FUo5$2*u}_=+M@Rnhn`kR~D;E4LQ$L-Co& zD$pos@-B_@c?IuUF=@*OJ>db(7HNZnvXo%j(qnMdH8B+58^up~(j=%G(Kf#I{K-jL zb!gj~CqcPCXZaVP3H$^jOML{tr3Jh`sQd?uVO4YgEc{nnwQ+MurBH}7!bgnVJ64vY zndVkc|M4nPSS~G_X6rDp^S3C-|6ypK;Xsr-F%eV$SREAJRacwkrF+wK^-TvW!68U} zFVgc@jFfc`7Z?HNvYjp7B&9h5H9$(|u5S0qESaINXK`=Zd!YX+>B2@!H1%nGOy=7S z#@*Y1bFzlS#m2$ifKEi!X%74X$K}$3^XTsIe(O~P7K%GQ)Ec}k2O*8E(U-$)*YG80 zTrF~^c=#&=(&QguA88`5CzZz-0!ewxXY_VjW!w(GyB!2B^(37hBr5L6AzO-qp>_3BS^E8tL3HPr;88-CgfiOLeZkQ z{_+%ZOQ4a9PR&o%kr0cn?IOk$nC@sXUSB!BxeG=r7v?O?pK~}QZpke7o}s4yeilXU z^ia$;hP3GXp)}Qu94<_yOJ@yglO*cOcJv+n`=h*Dp{%2pJ%jbWPPdORL$r#M6!O3R zZ?^y?idb=?(28;A5TvbP&7Tx~cM7*o9q>39NgS}9rL@P7Ur-Q*MnCD zz;n1~jc}T5<;&mf`Sp%c9N+PR^`xk$0{_*hz~_(b1Rq)P z(v$csT^2W`j{cc$hyay|w2Aqj6c-Q?c^>fqyqUcp0H@DR>evK-v+XphYiH#yoH!02 z&S|Pu$}yNz)J^mbdue#DzYXcyFoo3CVq)O5{pP+aB=2UR3dqmaaUnUkVl0!OrRvP2 zuEYjT7%!FOIMZV-eljbb&mfv}@}J&F%1Hz>!!o(Isyu-$(}j zj6KK|F_%*{**y_aSP-X0)$t3FG^5q@bi&(l)1#-h^?YJ(Ls{vyK5`5S^=kR;H-rrH z*aNPOk%gGUHiJ&0v~Lj>4=o)*@gBotL3PNulJ9^;b?eA*BvYdnP2se8@SK_Y+j9r_x-{r>m_5#X3|3oG+LmI`h+s0Xg27$phqC1V zwi9%3QW5WphQ&piXJFHSo|C40h%e~7HiE6VVqF5|Cd(Xz4T^Oy6615&l(H#4^S{{? zZy(vK>>ME=VNQ4|ZD;9N2sL;K^YmXA2$d~ z9$(RXyW=Kuo}R-9MUzD6e%w|J&XyVQ+($+OBK8&&1mY_!@JOblr`=};_P0U(<`0Rq zg%9(gWd&Z^jw*JAzmqCHrwe=VD_LKyP!g%n9dDH1)KGZ|LwZ3SOgB2SGaacU3WFZS zAAXnXE^FeLi_$hbL#=twAa!5OVuSbTkjRW-TH*3bSV|zUWArmV;5RG-zZOp`Ve9N4g81}_*Q~MF+^H_|6QsEVVZ5rrL_WviJmNOp zvn;T>i783nW$-GsM2)j6jf{@4_?AB6o#kQ%i*Q~$9<>{b_10KD9{HW9qVG`35dO!F z^MqYXV5W_i7mbw3t8-)c;C{#B8wF?&0rra7qV*)p4udn}mwwt|XqbR`v?MQ+M_;qS zHA9f)9MCvc2@ro9YLTbANW|12_qQAsnhj1n%WsXuhjg;PLU^igO=aRxM0ftKZbC@x za)T^sG~Y$qov?)?^}<&EmzqeaH?UtO9uPX^HDyDv8fP(}L6<^-zn^nW%_kdOib>Y* za1Byvb%uE@ulq1F-#-EwP-z)(sOxKIV|5gyvvUmLahs(z^H*zUnB0#_&$&F%1?U57 z=@*YPw~Lb7KeTu%Iw!|#@NCpp0so6S!K9oL3#?AjOlkXe_gJjU2dHBlN3os_z#To7Uy*`k@0N%k2jYhW)rZxWoB?INO8f0Fbgvgv_zk36 zzpvil*spez-|>-?o|3w{s+Pa7$iw6n{*Rlg@U#0Qj{&eB)*Lt85#PoHU$$EEI;SSG z`L)COWg+(J1Z|La%5s;ti1t~$IMIfM6JB6D7bpjDj0&~Cj5@pnY;cgOjCEdSS*GEG zP&n_xnJNAI!pG7y8o&H}{#)OPXiB3B$iX9UfFo2s$#4Y^5u4<1jmvw+Yg;;cOjN^? zS2mgb5!=h3joP<8t0Ql(mk3M-gi))*IM-Bk-_+85xyhkrJFj^)Kgzt;9R+%#CRL1k zPVh*)NzU_YmIs*2<;fGdmh8MSQr+k%+BBLno>X?F$DoBikIC0^)_e~Hd{~Q1cIlw{ zRw!og^ytFpSq$FH)%A+AT13E~VH$)TRkWkyL4atbA}zCP^ci5ToOmZ4FNOq=4X$qO z@!Vw}ZQH^h8?F7OIyD)$??ya-!9OKq?dzK=#*OXs5Pp$w@3+ScM&r(=EIT4QN^ZBs#$1DEua;IB@X7Mx0(fjQ$XZDzrxNpT-KM$RfHE5J z$HLWl7ZRm(g>HRDh$1L4Dd9iJN#(o7G9Mx;t<)i=4Q@YYCXFkmsl9vD;IT=?&Y@c% zD^1Uc;iiY8MBjP&S$9jncfKl~{(3?`_scl1b~1T2PapO36+eah-@cU(lI|%aCKdh| z|F(1#o6WW#gG=0GjTc=r6b{pPbr;zi_#DG6xwg$JJi3H_BMMI9EeG)^3gQ0OUvh!G z5O$EVtmc5FaubYgm6to{AI>ixP4~llH8U<`3dp1K>(MYa>}9Gxa++`=-~m+P-D7Fv zviA91Qk-EdO%wQ)YLvLYn(T=-Jw?BdYVh#cA&SsC)=SykBDi}{n%HD2i0UB}p?FRdL2mV6K zojR-Bz5!ctXBC^1AXH`aHJZQyOKcVbRHeIs_W@1Ot{u0Od1~~4CAGCNcm+{k{t2~~ z9iyhDD$-HXNJ-nqN<8wAQZf`Do&#{CZb){Kew)Mn6$bR|UXFa){X=!Ht~ez`^DBDvn-x<$I2 zF}y4qO{c}Jf|2fGdLut$&qI8e{mc{WdTS-vWrj$)2)cccLF_*G%vL83gN4ffbZZ97 z7b%!0%zq^ufy}m}yr$3^cd_9Z#PPooWK0?xr@e!F_yxfbwrbq`IahA9ohkPB1x5<# zOWEoC9j>SkLRS*2A&s(Sqq^kDk2s5i*t#2ms%SgN(EFIE!MmO34fd>qZPyLR26lKO z9}e_B*KetKPmx$-;NKJ3Xv@NU4>nnf7r&w+m8>(#3F2bE`@pNa6~JVtnUHAzyvr0( zfWpVp&CkC27$|$96Zc0ogu(_F-?ArjKpq!zfFP*M%w0Ld5AbN+kCs(qGi33-5b4>D zpo2%<>P-tw3#QplCbYLycl-&)3$_D$2(l?ExZ^IGlSWy;8QwNpl}?s+8)6D}uDXdy zCd~+d2Q{JU-KD;LL)63$;1cSND8MTdtqW-WO}JRcsGa;J^KNH1a*^B9^jM z@vIk>6CXTX$E(08J0}CpP{NyNMr6Q{nHKL(h>c6LXNf;Mw!h-XI|h>U8`^J~ahVNL9%+2$|9cX2W_ z8&$CHuWr}}H-tF}A1cDXB?<) za+E@F3NKvsF*j)TJ1?k`@R|^M09)@wZ}Bua$Scxut-DlyIiz2h!cK*$~*i)3uea+}`64UQFlpK^y>78O0y(qy(S94W`zbg||B?cM@z!Zr@ zLiqkDAq_+rW2}1IL&WDQwtvADYWB4Ym!V|+4ZYG)#(L<%4r$Zp7gqx@6sC+JTO;A^ z-^W@Bng_#vP6DYO@A#nY9!AalY}Vh`@)(qTQ(Qz6xSJ<2`u^`f=#T0i2fL;L6aWrg zTUG`uq&o*#Uz^G{FzCtEiLnJw+}cRHG~PPJ4z9&h=%Z|v3buE@4~biQy;IptrFdLz zUBeA~lUO5v9KB}{wBUP*K98SltF@7BvP(7jsaCxkGrSUs6E~tZ(+Qo1kSKkQPsd&> z?3G@3(j4z0RjjeSb9Q`n&PQPx|O{vCe#-ci+o|>PGJ1lH#_KQDDw zTX6#5{2Nd9HQK!k&e8c*-nxvFBXY7KFDj3U-#|oQSM6z&&qS-JQ6||&ND#5DYv@;rZW%d*IaebaF@2h~!GQe;Hd@`@mUwEJ zj`7)sT+w%*H}*UTY&K_WK7?yJGL$CR=80yya0TGNM-*KMUhuZXJ6_i-Av4MV#AcSm^A+gN zl!*yV8hq1pVz17=cu-(wkRezEw-Hp2f4O^MuerVh@h$EAu7)2r7X?HFpYV7}KL9n? zX{C_SNA>PqfO^`oE#;$)J)_kCvC<|JyO=YAyjKvgZb%3U%H3qQs-Tx~A)C58gGPLM zJ41d)smrkd1?w^7m!lHkxL0)0`Z%*LPSA{Y`@OP~eyrByESeUC%Y2Uug{bK~GYYc2 zl*^T(EjCERpcp5I%h0;)B9p<-(S+vS+09=2=y3|uLKg&h{0yMY-{6zERHZvdV&&C4jnroNS#43DOgk_=tiyR~2pF zWR4VnC)Ge9J#h-jEX78E8w6`yz4M{oP-(#Jm%L(#*FT&Y9P?RQ4KtHX`|-^Pa{8ib zemf5PA;C&vBSlj_qw=k!3Xfaky&@HR2W2whba>7EBuT#0fxO+z+HlIB)(~P2j*I4Z zH2a|0ueWWIZUNs>NLbAOsg41fV#EMt?2bRd>9uH;4r02egz6``ufM$t&x4_-x;PqY z%vTv5UK0Da!mIkbWu@SbIX$2l9g~{{{fnU;1Fk>pLTNb>%?wO0Wx@ z%xSzWa)MPG)7zJ+E|u&_dvn}Kyx8SQPo(L(XS}a#R?=d&Ub>7ufgD{m{#S^I)5v~X zEm4=(VxTQ|E4F*Qh_AwS#hu8QB1WaXY-_kf(1kDCT7K?$vzL4Q=R#AaOg%G|FNyjS zfYpSb-z?g4gNZs%k*5CuH#9SkaK2e!lPGJ*US^l6s`cnq-oRr87wfH3h8dz%)V)ZB zj&C5R6DQ_J6+Eh=HvMC{+khGtJ-e>z7K;75Irfr}g0NR{RpX;O6&u?~Pyal1feDK= z`AnbYII?cNy4|4p*oWiUm(6K}1-W3$FT-Ms@s<~QqPu1$|;x|`U zT-P_H7EBk!fw@#q<@v33dtybMl?RsQ`o~nF>l1O_#Z7a=@e89abqFjZQ7Kpp4azg? zRk$=#m|Lj0?(S1wlR2O=p?r__$47#)XZ|JG2)GjpL2&Lzb{!87h2s7B@*s6z*GzLn zQadi%{Ib5x!=RHGE+*lj`xn`OEN}xWLz7|V!B|>1eXAJ>)3bc7z5GYu$Gb(>5k`oh z$tK^ZCVk2dkgoOY<*>>EqULz$fyk=|TzLUiQY4)J;?}Y%F(IJR{Lzd(o+Q&PZ$LG8K&c zx(#Qu$=0l;U+wNTSPG@CV$CCJQ|T#75+QAi8`bCO!z>7x#YjoLTwZF7BT6vWyeB#( zv7a+W?Ur$)hmY@mEWY4w63?U$QcJ>IwHf>o7;vQcp!8*4y?i2G)n8XTgS8Ljk(6@y z=4#cWiac*HDLl!G=(Q5<_cd?sPMHWjAxyeUt*o9EY@Iw}&i-Wft8Bpgeg=06dLf*g ziV7t7^pcSc3D?&xluOn&R8fBm>DBQjW)r47rY^_62q?&|K zW~DMZ(Ol~k$y-0uAtniBbcy|G&}4wbTh_L$b%h|$ zdhFxrNgw^b_L<|iSuD&_F(TZM@C}ov%Dg|ct<(r)zqU~R%98Tpbo|A?grHu!t7BXk zA|=I3t1OTC)MiG9#Q_e4FJp6*UvvOpME8J`{TvmokyT~1t*-LC5b3yl%LJvRxb>m% zM6_r@j*JtXHooLxT&th*IY93V;c2N`<^)6j>i97F)xWv#xvOce*HdLO|BF$f zbbo+Rs7!FN!Cpfz^Tbo+0_y}hPZXNEIp1bkIPvF#|3+0r^$Dx#SwgJ_m1p%C2<~Z6 z0?P8L6~*RstJ^&Pa_t>2^085yVK`O#J)fDGz~oJAXLLnrM_Ik?o@1so^8_<`PvF0u zuSnhT#w9hTPc|{Bia{nrMf#ulkHGffgmfOE2tw%*Rr*zt9>tlH8i=3)Y^|GpWZ4i8 zA>ELx!UOt>UFwt(vVPMymM!PSBjdv*3DZ7$@`)}6puW#Tt<@WPo3L8Lj>p}oE<$Fi z)V!S5m)~#3H31gH_5Pyv&VA4;(I7~(o$&o1<0U2`zJvvMZ}~6!F6N81uvyP8$ZRfw zA{Mc-bHgbXP%0ma5OjR@Jc_goY9oB0jNH)BUv?ELbcT0vnl&wtP8MQLhxxedQxZSZ zCWp3n@3)@mNA-~ld97SVrN0svlq>$R>Sfp^)NS6KkYz@uMB$`3mzv3eP&?qSYOQ=vrS+(Ek z3#3xbkr)j3@vd9Kt-UI%y74c3@!~%qWF z^6U&hs@Nnt=(eRi(`aWc!OsiPzAp=@mm9hC-FZ(|I|lz#0N{@V>?P7LrLR2`D+iVl zt4&AGUt%829UpqlhAi3&tN3AoP!UkU#84%qxL|ls=X~CU z6L@hC{vjhzc(5BqtPp7OEtwL8jy>m$sJoVnZG?Y|dUKv#f&WOnMARh@-c zNrc<{_=XnDbD$~X|4eFbO+O{XDOi}COHP-aNmlyILcS=H zOcT;@Twcx%U$x2#n@?(Lyiq2;KL*%774*mR$D^U?LUOZgW5nQnab^O0Lg z|G&96kQ9aGz0Sxm2rg=-I}yiu=}-KCLKXt5aDafgfN0cfCn=f}; zLyAt&VD-N!`V&@-kb$|mM;)Q$$V1Xp?p9*JsIXuCKc?O?stqpa77l^pF2!An6}RHh zAjRE_TcNnSyHlJ}Ah<)(;%>#=io3f*zVN*FzTf>D*2)U!%$Ys2XU`0>v^LsJs^N3g z$$Nklb47P1=Hz(ZEGU?k1=A2gR4?Z_+s4r98}cw81D1w>Xl-n*=UQ)LlfnA|3kCV> znN#ILf1Q~?a6GUYXWB!4r}{S=a0nZQsVJKx$Ag0EX6cKAsPYXtex$oDIDhwSskBYm z-F5^iYx&}fCs;N&Rgx&nUg96#UNH>SOGy29HL}4!#X8{5aUK%#9+>c##I+nmOE}5Z z%*RRC)-#HhDWCnz-&At$iK7SH&0%NbLqV3K(y=VD=wLi&uYtca@0tU9l;SkoqJL$J zn&E%#O)YH*nA5Nh;lC`kv-_mJt#q1zkkq4G$>Xd3>Fm_J-y&GL=_vdt=O+s3;gaH2 z&flgCY*eF%Pcp|wdd4O9>Mp97%YE!Kvp( zwyU{m^x5F?ivk?lSzB${8e_Ob6rgK<#}7*jjg7Oo2A_xUu}0LEGj!^VMbF!Z4{l;N z5@_iISa&3a9T`6d|R@AmhsXCHzk|;l_{*@~>RJ;rqv}ySA zob0O#-*NZ#jJhsq(5}Wn{vwS&*>6}u2LW83ZCX+3gV=MmjM~FRuzp}53rV@<;UFR^ zYy-=jVGBbas_epTvc|Px1}^rf`~aT3IZj zSjkd(_6^-}`>zDLe^~&T&*h*Zi>OuKp%fN&Y?#;VS=+bo?-Fth@1G?3_iY;;31C3; zDf%sDhAnP~E7~SAAwlL)CsJ3MH#n1kf_v3XPa9S8f>Qu8+L-|5|0|!R*o|0R$HoMo z9>~K7+JrONZ>k<&Rb^S`jfwoLXi26%SS|E=GPM9Nu03B5{on@;oRTOWEs4&U*fdhM zkFG1Gt8>&|@!g&kYn78QLj%t|5^h6zda`#YP=@0_q9Y!J1>HYR-ZGahsq}e3Eqg_H zHC6}a{na81FuZ$&Y{>dspO9(9XSf9z%rHX})m>Ba<`j!s#79e%!(zB=*;T=cXs z4YhiSuLItWLsN{rcq(01T)Wyy2803m(W1DBN9V9nYe6N?H<2bMzYN$m+p9;gXti@tHMrF?m3oSp^^3&y+Fi6mK$oNKm%qb~Z6wd#hI)oO zdFO^IN_7M8-dEvr;e~o7)YWnezQ1xt{9cs`1KZ*K%KeBBdXU+%Ik5!c-fL9;YsUGs zZ(wRw-WI`lqy0=5!Flhae3r?^J;W5G$@>Vyr#HnbYSK<79V#@l>t(fo9B-vR=af}X;FEUvPy zTuFNzHM@!blGx*}gg4R7Zi(mt(V{zk%w+&CvMY>(WTr4bv2on^cz<&I9u}-FAX-+Y zuDZo(PE;GOCbsPo=U<%9-ZQ^oS_TZo3*7okQc1^;M^y^YOu|nmNF=Wy$(!9k~QS*SDm z(^Jc2#w@N}=v5f-jzU+>53i|>wXIMzmcWr8#ETQo`DQ=PsmDftXSE>C>G5a15WDyi z+gi-gQM`{lK)PQm(cxU@A7H?+#j4{-{pEl* zay%DZ2s-E6nCRZg=(LBOYW(k&UlXyxcDgZf_W_8%z6eaqt(oB9thBG-{yl2EugI#^ zp0iw|1kf!-FpNjNJUr%%J8-1|a!F6zx1IKqjg9~n_5UK;wo<4Qd0tn?6I(Ml>D0g{ z!QPV%&T=*BAd3uR{p+N)R)uS7Yv;c&!B*YJYyXQeG8a5Iu%~(~f=_IhPqn?7KuqcfH$)>k)XTH$=q~?>=`d>_38KrV^-9$!OZhhz%Quar)SDUXz8Xv%4BRi0SS7kvWUHVGYl!N z3}RJtt^nuTXqB~-NqwtGl#9}q@}Wlwd3atMU8~Q3<~dE%(&VG2I3hV;Lre(@yD$W0 zFPNy~O|hy`8QVHmk$&PYHT&rPARy=u$`dJhMNHThFj#A2K76E zte5HN8AIzII$- zPkp32#+`yqO81)zZYKG-$B{E7EF_`^*4fGm=+{jZnB+(YC$uN!bu zQiBfG#h9F(N!{7<%0o$B(=;6KzMx*G(!^F>$jkG+wsgafoig{g z%!daUX<9~;CpCGNBK+-uYt_b^>c;yus00vGkQi(8TLkSh9Q#Oqj4^Pe8~8lF0Q=(P7ACBoiKw8^=V(pZ}y3=B?H=Foue zG?@&NvC93$a2UJ~3b!X>x}Euc73*vRf?)nFYqOijckmGWWCcDfnDGQkDG?tu)Rzo> zCkL}Ku&?SY_ea^kmY(lE-RbH{$q%=;k%yCIa^PF5yM3!hlskFgVsEpus&;H69Jq@X zz&=A640T4c%j;V3hj&`HsrUOfFni77Ve!}g?l)Y(#XirG_s|H>*_oMfA_+?Mf$Mu5Nn)*a!3$K z864iI5Pw(!=O^#1ygH>biJa?Kg`zqnt|;N;AT}dVowTx-HbKC)zdtjrY{`P>unogN zo*v0f#`IRxxIS(s6Q2HDv1Fau;T#@&NkA{5X(xVnauR+a9%==??fzoY>*?<}!VOkK zT-85kuiG6kJ4eNG4O0xH=k^^d)vr3N_hn3x$SQ=-2fngy3a~hUcPLJi^r%NB=@|Lm zjxmm@93_t*m7UMVjroONpOt^R?>S(C0yhjJ5#YCQ6!r94ndwR*8PcOnS4+)kt*{sF z>+qdN>$tmrPK4Ex5$**BbTo^e&ny46*Oyz=5uxLqJQ;4ZmStn@MX%BR1PrdS&#=`r zNG=9=Orpb#I&;Qv*qN#@0R9NLh-`6F7^n=b+<0y0vlVW{F~Kuni^?;fj%f@Aw-W? zF}l;}EH6vFYbHY}Bx5I4+g7D)nW+_g3@>wKcdIc?uE>7HMr^ zurwn{0S+N`76Pulel_)rc7jA73+1pr-o#DTwEl2ow2Gkn+E>R#F^J9+^{tqX(146A z?VkIAVqH>1J3K-6y;{N@F!i!(?kJ~aWU^i4u`7q@_T~J2oES~ZLm3A?xpsXd1MUcS(R3TxWKA%7b@r~1FICmkImaDETGnCDF{#)1-8rk}JU z$YAdrPa~ZHv#R|bF?i(NVor_MSW@iZZWEOHiOc{8g8GgYYo^ISiH?=XKx&u04=wt! z9{BZlg%YDQB$^K50)-?}V0w3&;hl50C~5xD7LKlRjG`mMVCGARj;I^)tGW4_ry)H_ z*=r$=&imw!Om33?A@$uyJk?HTo97szU5WpN@7_rNUL3q!5}QNIt9kp>gIMvv5%3G-hU^dius?4U-46v&gw@c(eig+ zK^=0b-r3zFcG>S;_DA}t>PDk)1G6vAw{UBOXT3cYN6jimxtCtCXfpKmdnnSTY`(pP_l~+9 z8+wMK-_6IfcOdO)i-R&sWvNq1;@0SD9O@O8+JaG>XC1<4S8g}2R;y}ehXDTrf_T^+ zFDsMNfYUo%lOC_iJLVd)dbm@~UuV7l8l>l_Qc2gb%RjZaYI4y>pd7N0;DtmCEvD@G zItTgFUwNW>Iw<^9>qBy=^11z|;X>*^sDJq}{(k5pIghuV%Jbz#!>Mfp;5pl<{i({$ z{?%;ScEXaGS0h$+1mMi3#G?2AhrJ#Ym_d7%j9UCCbqCj(nwG;SCBHqBm51zQ;Kca~ zZNGn`kp znA-83Lm=AQSu4UZUwx@e>Ouyv%gZU_p2B+^w(SH{y4H@4im(0)r-Sgge5Rqgo(us; zO)4?FsgZybz3>)Y%GNfBII5^j8fwB>di<9U7NpnL)NIvDYPJNxu}=c|w*diKbc!Vh z=rix_^WCBg8!u|ne}@SMtr?^!j&NIQ8BnaE)WT7|_9Vt*#@k8%MB_Wo`q!4Mkj_yM zSIV%$SRFF`H@mn_b4c#73gndeD#&BSt31&{nX0Y|4iClZQI|(LX?5+CV;DkoiwpBI zysR>fM`iy*do;O^sdDD<&tVM4@~ScCv(s7Opwj(fQJ66efl2h4nJ@GDx$`@vzfh*- z|Dfj(d;mePROGrxv#5X+e%lAQI+=^gtMXf+TZr}ZVuG&$N*qAe1l~L{yMUT`^za?^ zx_j5r*6a%>Tx&(2wR;`n{Ib=zKAkgkHS#}IiLMm3*SYDehu-ESSJ^f(spfFl0zidv z^ub?Ka%lFuj?u~|0NoCV4)R$Ri1%{M(qG}Yj&B}Y%2}Vl7$^Em+tgN_bv+b6He@jfSZSe7v~d(ZTjHa|((S)=cs?3xaUwiK^w+dal|_w^G4l@xZ! z4aSk>x^&bn_?=mhZLDPozn|w|qk2r(<-oA3n_=_R-p#K*9|I{F>42G&iF4^J)hYIkWvie2)XqYhIGS(I z;GP^Vy1_Ef4e8T@>I<+JcH40N$g>mS(*v*MTKPw@wpBLMtXo$8X4BJYbftTaz1Qg&_sLFIs#Pq zSSXDBrY(XNLcpWb zr6a^RDe6)F?vH%ZOWt?tIx)8AmwzJZHY6uWnB(U6$^Roz^T(1-K^(jT;fs?PFyrGF zmpLVk3uo6o3fW{MptAQ*HRNW`0f)?`mt9qU_p-3KWB+=gokBh13XJotJ*j>tFixO-H)>_t7< zR&}PxgB}R zzt(n+DL1OpCcai??!`F&9k$Yx_@=Cc_nnzUUu#|x$LFytp53=1!h8o(qBA4U2b{7Y zCae8fZ1AVC^07`_KEMveV%lP>_2G*~;(-HM7O1czqaRW6V?aIz`I!+I)LCj|R3sjt zIr!rk#*{a5l^N#g=d9;te8i#d+FpZmv6OX4FEtl2Qz6=bS*@r<@yNlpgf)W(^p&4~ zhbxaLPk3kZ)QLFA$$p0m?VzyuD*S6bOxiE95N5eo9qha{jfNS1PINP&ch0P#4$`l4 ze5|*q;*0PliufIp_81joTru?7Dn`Y%oz2v42q7QQLnEP=ruh!C__3&nFI$jtgRc8E ztc6?RLNS~FrIAr0-|f$@qPD-eO=}qPoO~Pf$a-W=qO2i1`K=Q7A%aYbX+B75E`IMN zDRQZDn!&|KLRsf|%mmx4bs%=IDYy2I&hX#M)^sx)S4~jNrNE+E2c%->;<7_Wai@}I z^K0<&TOPeLH|N6jE5spt#C@dzs(Z>E*c}l5mnFYJuD{Qi)yKFL(UjlV7wuB2SbNfY zj1{?qAANCtlvMcsa8#D@wDhsoM6O=_Z49k#6k%3VFE$#ciER^86=bZ#-;U2JT1VgM zZWGil`2}N{%*`~x@dkK4)A06;G6vFj_cEB>ubsHt)dqJx(O*xVaWE;zVlRHMn&5z6 zb=yg8bWqQsGpo6qJ_mtfVkI?`J$HDQHF56Zi6*$$kVyMpwu>C`lY>_&cr$!}xK4p* z<6j;|q*tEI4bBcg$UxPdsiwbu(bgcC*_X!ftl|(yhEeTFxIHI*ZGZb;Ti+G*V<&oj zoIS3grX$%IvsdB>7{T%>Zh386Rl%6#|MvG=AyEV0Hesul$w}}DW;JZV+(5Z8${RSo z|2?i>)p5fT^VB$=Qzn|FNBU#D&n6&)=HI zqZL1f9+}j^KHJr#KAnvx_CTD%NvtASSMf*bIhE~mDKdft1O)T943y z1}b?PvR{fl627O2IC!bKTStptig${(Z0j{;k-YDi`Z&NFdl>u9xRfWYVH)mHg?pF# z-4~Zae-jI20RS_Fv2G14R55v5P99Svhg4lxag6hCCu^>#^tINV*?b!HFwd^%GatdP zCr$b0-|CDLuUOXq!PzSoutb7rI=v~BjQUS_!`xECSbfxfZ~c#=l`|@FEuKd-BG|Cb zrg$9oJ~j4Marv9s>L-J_78M5&V$t74MF;xV$z2a4=vRdcz=1R;)%)g+pN*|@OED|Z zk-p&vMcki|xpCxlu?c;@DUR|2oWgc|7r^4g=4RRD(LKAt>A#cC?;{2N5xcDXKuCAm zKcf5gK50Tl2}l5j6uAWJTvp({soSGH<%9B%8^2&5CYF{Bm~ECn&*^H^#6ZiM#>5*U zRcpdsHa7UNG8-gkKH5igORAb!@-5fq^vZN%!^E^q@pN|(58CJ0H+Tx@2=OOe z_`fH)$a=u*n$mTy1!mzJ!)?BU5x9g%qr8U}MCAChrv4Gn-=t>p40s1&L8n)h@K-+l zP_`usc@izn`Nz&3ZslAqJUSUee-koe8?YGRpo`obwDZHdRIQanaFw1qz88}|?qw(d zAg*~q+XfcG-0CA4hJO!E{IQ9rnm1QjVAC?_`IoT)3knI}sh^K;mL*t3+c|d{H1joy zUj!1O-bcLK2UcfDDg~>-rAOTlFaX}m{a4*4r(BPET*XvIwK+w`K3S=;jxQse+<=iH zYF5S*Th0$rqIiD~Le-GfZ}EMRS`4Q*l^lFx4^euy{j!>*1Iw%_&ZO}9g2I4SGw&DQ z8(~Hx3J3Z;sf5StQsG>Y$hyTZ+)${E+KoCtY>-)6X+5du6%!v%-=0Fvz&)8jEp^$_ z6XELJR2z$u014eP7~9edAVnGI*qTs0j|=(lTKs}U2|UxypsUVRUH@q5Qf64@?N=}4 zE`+Kju=TL@Rl;Jyj$<=A_O|+x*(J z7mjdIU*vw=pn;i(jyfzxjCOWk$O?q+ikQ_tMRc;dBtU?x9D%UgW3-OO9QfR})hWvk zgeFdS5BRG4s5_vQ^`j^}RK2xahR+zyYAwUMjz2ke>W#qp&E!g`R1UsP7~WVHq%K`P zV&$v*;x*%pRhl|`1d%K-7C;qbXPAV=5`upNmm_chMCLabAIFT3Nq(X79OBrnIGG^| zuaaE0yD&rG}YSgoxXVF14?7}9hyReBZ&9#VFhVxzFa2h66F zL!(xUl=uLMKEujcng4Ul9Wnl@>33=8epPN7zPD{6ry{4qpgO;efa6oyd+%R7L8T?{ zMuq{!w|gZoA44PJ)U(fJs|xa1!x23266AN@|0T5y+f&gs zeDiqslA>zZJMq6h_iIyEfKRJzT5bJNBp@vRT3SEj-QnK!TA1_35!H>6l)3z~maY2} z3HDmw=<2I{2YOOCfh>LvD>mOD(k9`y*G=1QHy;zx^R6GbW8XdD1V&)MuqC2KH?#PB zX1OS9V$@K^!3tHJn>4aAIZ>=GuzC7_U&Y={nXFjEa^9z$W%hrgCJP6t}uoEU>Fc9DFh+FN7 zhSNDYJ-&%-hOU0YBvuYpcnY7p)@czg6e#X8;60xX3mTed^+P}8_(d%UD_1aH#9_D=WL;}{2o(HqJY>Z3AKPLv+Un-s`pd7?gUUZn_XccvROKNoLLu>lFv}7U-SL z7?v+}wn=z*5nwN}7K^QRhof_syD(5PkJ`#%r^}h`v2E{Fyam-DIR_N7uP`TsR>UVG zJ;C>Dpo_9OeP-Mcq&ot{rr<*NT`EvX`CU_9P4Bz6fB+>4f^&iJXAHOHaL3B)YRp`p zv@Q}Hx=%l)tSc0v_j%`=a64&q$KP9(a>BP>hkF#bero+NV_!x6h*r7|3EL{B{Wbz6 z$XQzq5x|c65>k=U=RH!WydJoV1h z>v@v)Sb1#ck^eB+mI!pWdED9k4Q#TjL)STY%qli5P2{~EODie5CSvt867tyru%O)I zyI*R)xe)}QBPIdBo?*zZTd``b}EK5*l9AEj>}~+^Kd7jgQBA#HlLElT(N5q)o01E&O5(P>Wy=qkx=KI z$O=pm%i*1Jr7u0JQ66w=+TdH!@O>IMb;Cqz{cxzv-}wU@0(U)~04YkSU5>E9$t{gM z`?hy*)Q<-q{n2RZ^p)C5{kNHu#kcZZgoHk3Y}c==_td!u2QC+0(9?IXK{UlqpLId~ zH6|#dSa;ByB05j4S5!1su0CJV(Bt%v>U9ocXS2}Eibz?>7}nhoupHL3w<&zNQ=_?3 zA0ac(@DGIWV^C<|g=aX@@o;<%m<$E(L|!16@s_;gi*sb~OeZk>{Z9PSG5fXZBn%3v zz^0u5@c0gT8avZrP@ypDxs!%n_JS+2D47LPRb0KavVt5o2K~eZ;eHGEgs4ewI}7nQucN7WX3Wirl48 zl|XI#9+Oeu7$&VO&WS4-LG@#^E1RP44VpWhgT%z?OfVi{bM6T=QT&iaeg7i1O2d^O zY5t|}lsCx3t_euy%$z31ug`KOgY1WYizmM8+cyo11ipw5L~v9l8WZo4>>WTzQRE zYU&AxLi#Y9s0*w&om~XLJxe*^#-`YyoDdV@W7Jb zp>-@QX*{?*g!v6cThMcy1>b8ZR$%9|?Fz!ojb#zya}T@!xCsZ!wzE~$DZZ|b&6ks3 z64%2Q=JhK^_Hb$p9A6;Va)9t5xOLYY2&R$#Pb_IkM8qVd+rC$_M1fA=zI+F@V2v`@ zZ%lgYgF>QE$;52?9xd9s80}jRJFF_oJJ;zwGun z{bW3M&;Ph<2nEGb1W)OP!HDhryXM%z4#c4|@%&TsUzrJxBB+oA9XvdS-WQrR$n!n9 zH$^*^32qOCE)yHRD<~$iD~S zn?bR6kHS!D|BN~@1^dsV-OBQpWCzk%)TRB%UKc_lZ9T&6M|Yp{Mv=o$7|p$e~aYmLvLEe9}ZceIR7*dEn*IrC;)^%#!m*G zb3A+@XOuiL)3q`$q-CHs2tiG{cTXyEsk!mfaPWWA7y`B}zIVqaEb@0ItbLNp{9qLU z+np-xs@5O;!h8t$*}3DCFeYSz_KfuwLK?swTfHz%${=7YT(R#R5#{XKG5@}3vgV=N zJsTopoK|3>C|oB4Z)Z!qW(SowDC?G#hLG#~@YNEd4}Uc{W;wEzQtNPQTY7W{!CU;n zPfzHgkMta4yf=As*+qz2C;aucIs(HHRxgcr+l~PnPVSvX2E3@wqq@3sYne zEvyInj%K?-UNdr+y@d(eE#*5+!)HC4sop__BuJw-4p%gFMWb*ObvY6i^Wk-b1n-u4X0CM;K80Tx>cG3cF+H!1R ztXrSG=`;`cU6PbvD$tB@HjH@J@7?V9|HNicJ+^X`RVhPb!rv@K`Of)OJ+Ho)K}a&n zyl~+4x~K3D^adpufDox zasdEoKQU#lOr#0f){-=UX0~1%i}G*l7!!H_CqCs>&p!i}uTA4Wal_OSjQ{-Nu@{&y z^$xtOP)A&xVRwBGXU;U_EYg;A!Cp>~bYQ@_TM7;p{-&aWaZUK(k35Ht$Yq;7C5*cLI#&w zbK#dB^$ZRhJu-pArUhzv$)E7HVVL!}?E2+28$TYm1Rk-#%|Ey;37H<{@n>tZ5b*lr z>jr#I!Z(wbG7-)zmSIQ!*U}zi&8?ib6XQE!fxR>3G=`Z~v-rDTt~tbk4TnPcm${&& zBn1y9CLYL!8zTXSAyKCb<~;te`&m;RW_RN(q#E1AWM#-K3koRN)HYCPsYso(j%fBzY7`|4-u<8sFVmepj5 zNeshE&}9fz(`DOU;SR^kKi1pM3xB2r-Wx6ky#&k9m30UCx@?FUn14;5QrM<>ioVpQ zXzvVyxNvdy{pXnP_YlM1ZCWH#5+_iB>m9Ev4m+ANP-6WNSVh8f=&Bou_qmfy0IF63 zaktra+f}|s0wf)uOA{RVpo_1y*)heP%2^NfMQ0v@Vr5C}?<2VP#lb4@=tG|cDmU z5B~!;M)m7ZUj+~^D?McBdrxIpqS13(!8otTpCW;z8c5o}K?|C~CVxCVU8BolTF{KV z|4XefFsn&ymUwJph95*W`DA`0R@OGUDY}vOy0n7?WU7pPV16B)OGv$Dla*M~5HZsE zKUM1H@MBeI*PEB1Y*|i}|5B}zNaA*y+n|>!4KHcGdx!X&}N_nO_%!RqK2w@^91d(i4ib+cen{sh7SPTw~ydfVIw10A1tBM^{oH2OyShJ&N2JM|X; z47)h0X8!9Sdjblg8e|qHu-&Pm%Z2A@)wmSxiC;2`tIYbZR^+otOztS0Rtyn^Awkx% zlW#!pOju)L$8c&?kqr96DWQEfkBvS$YC$22|4twR2&0b9hj&V(UQEi3 zcT)Vx25jifbBBQd5CCo-i2jD9@bzyb1f=n2UTO)HxM36bA}Ono6kaTi+XH%wi2#MB z^;~b#e`$Thg@jHOaJUu3g=x0p7j~%MgeE2RSG0-Ovo9l#;@k37WttH`1|ie?MLe0P zrGdh_csPc-={8I;aS;lpA`fi=1D-=!w6_62L~VO)w4&*j4<4>F?uDS(L-31;d`ZEk zg`-X}U*MsZ9eI8>)aE6I$lpTb1}K2p$5WiRXgS?1(u-#gj@mCz1F&}td{;k-2x>&& zbrGQ}C02ccJ#hh}7I5Ta8X^+NPGDYBO7@+IZXHgXyAlKhhx;#=OXRESYLlu5aZn`` z(=GrgFg7rDB;*+^zPZ9_iS{NNLnYIcFMB6iADS)AeO9#2isNt_C%yNCu*B1E=!wua zJ0hQ{kqsLbAFj?GNZ;@h0d6Y7c^BlzlcgMU(}26mvw%p&KYOA^Mi?q>h-M>H0E?*+ z%rZtb;h9Kac#c}0HYFo~w+s1h-{a5FS~?+of-pmWbg(b#(FK=AQ)It(&L<+% z+64;q-)HbFvG8@49?{2pz<8yhiF!h>ENiqYO6@aJ+?(SsE`18GEE|C|;U5X-olNu4 zBlMNTPj8`(7&kDlG_RzIBc8T-%CQC*nPD9y+{rrYuA0+{u8`BMQc=fKU@E;gM}3{z zSo58H&oFjk%18MLGx2>ZKoz77C1pZFKL2EZLImNA-%|W2Kq=0V_G6!-4!46+ej(B| zDc;N-<>MMGx_>_x!J0m2>IVLsZLmO@Y8Q=MrpT9nXJ?^*`@^68h`0vYg6K`BGi5p?r0b`i+8AL}GPmj*s#)+5_) z6zP3@R=mH+G>!F_uVJltR~Ejw=vZ_~1Z_XmsrmEE7%)Kz2glI~6CdQ5hTi2qmW}th zS87-pi~HlDg-*oNRPlO`tq3?Hmpe8Gf>>~xDv9sp&}Q%sXPv!&T+ZLrH}LB8gA`o; zZv*fJ@B6!|>lxW+(NU~h!BEA*jq%v~WghonaFB+|5N!v%>oDR`CZ|3_&L7lEMx?fyHNU!KPE>;4j;rM&5>%VLerbK%(q%f_DB|arfVU<54s0)s z`o#O^Ic@;rq&aK=^=8oKGr2mo8Bb04d4xj`a&P|p%qRNFyi?v#Sv}rO-HN#M6NoUL z9A>!I9cez}cQw#zUVW^PhK*?B-Ri*(JavxpKxo1BQQAgX4m_7o3Ko*_iXbBN0>e4s zK(q_2gcq$HF)f;A!>nt$E0^@1nGAErwz2mEivjLqp$jnQB!X-=-vp*x1Ks;MEh5=68Awz|_$1h(n)cz^JWamTArznBT&JW7+^~ zMkh)_O1O$j9S^`f2O$9nEX!h8W_VzU$}_y3ti5L83dh~_8Zr#v-~ zw@=e1sja+Hg!)1;plUvemh4O%#KnoLIc9G87D-8?Bk~Qkez4A~*B|a&Yq7VJUyR>3 z?Em~R=9LZZy=4FwxhTy{MBOMH0sSKbU1xHxW~B@t#~kU``C*|r`g3uX)I5aHHMMIE z_?Q*CMot`xPd^u0lptb>s+PweLDVZH14d3o8I@V#*2Aq zEoy0~&@tRH)*F&JLyYG8JSeNc*5Sed+bJG_2PM<700@>b!xcVUkP8p4zqvWMT{|nR z8!tbletbg)?zUZzmEONk=~6+`1i2`Wa($m7NXIMiIO&{FL|Zx0N4Kq39-|z-@Qrn2 zKt%_9J~XelKPCQUk3koA+(&IFFuaO)MlZE!wl(}@CDZDvWhn+b->AISROSfX=BzU- zVrABGuiRSA9jUa^h*|8;^)$)Km29d0G^=mx{EdX$X$Ac{ zq-%`H7!I2ssL*E^$VW>cj~Ua6Y5axYt?Sh`F6acYy3{}g$@+@a%tSj~SQMthhi2yk zm#qA+K1}a2J@}tM%ton*#(uPd+%vJS)K zoVqaPAjy-S43(aA*5(^nYh|J#g7$k~lycbX?XBRe z@&4epnfuO8hCM4{a95`hM(;E^a?@)ohu)np7FVr10w>Kikb>~Y3N05yBqT_o(|$49 zl~q%ykv*z;I%){#hv4ocT(b5b>c1c&(2&(wr|MzuK_ju(nx_peQq;n+u7~JmP{F?f z&m27p55Hrf2*!2KGZrsL4$;j|t%53&>uVGgtecv{v47x}4Zf-0pw>rgVZ*;sl-XN` z>@ctUexJ0*=yvO;2-l##-&X8waZ^vV%JLt z8ie&Q3sospEyjQxn+0`qV475hK_Uf^DvGLiOAr$OScV8Ge4p7!Y;FHFNn}~qWpqLx ztBY=WKwb!E=MqrVh(7+~Sp1(1~sJ z@dpS=7$XBqS_?7!k|53)&>5OAoEuR`kr6GD0vGT`AKbAm$N}OkCp?DiihVdXnhimY&lw@Ed(TQhUYW321jY(R5GdjUwPQE@7huJi7QLtYmG2vEl8?( z^r4r$jXlcdu)3!2Adqz&c$6bKsIXX^TXEc`nI6p4Zq9!9~h>#g12k) zQw)b9V=oZ6cO&F9Q>oYV>ZQ9}INw@JFKTXX8w&a@!RE=CA1B*s8Ry#eX<_<*=r&8G z)fxW#`9L$3wPOS=&UbQRRUZ>H!G2l)qbPswcAYMJ8Fuj>IRtaP_gy6oLwpC=WXW+aS^52k@@L$ zf2s4YrMS0StmUIJlOWNNAzf!}z_ua| z0?>6^1e^P%E&P7Vu)m?Y=&!-FcegU_=~xVsoM)uqVEj22$t#vbaRMS zz3Tj^ENU|8GbV=g3;;840$1)6KuwMfDY8LQx8xQg9gwG4kZCzz@z2ZjGZ^0G$WLq( zi%{=q)vN&_x~>r83nuJpfvLZL)gI$qhUJagw%=XW*Hkl@pLhS_6cS+Bnb0Rsi^qSI zhOhkcUAv@SDkq4|4CIYr=cmHt>lgy;>@%G!yw(_VlK|YdJ!Wq)$HbZt-a6GOA}H*; ztYF;}7}LFyG7jkygk*s&UFiP7j+ic=W|PZ{mcA~y5^5UmkBr(`kA z3;ABIrI1R5n;>|CtZ+u|QY{%NWKgHCpB=YRyhlk|I`xR0Kb$Yf!`)B3GuEHHz@#+i ztmg?sXPV^a9;eb4Y^WW`y_aeU&FfD0MZWgCzRHQOk(-eF*E+Skdh3uX^86Z*wN%B| z*mm-yMvGqt>V^Jus9&+{MdB%O6PwDio)4JUd7*L($xeo{GF*xU&g28+I>{|Lfe#*e ziH5gije1c3)CtvUldZ$tvs1k`Ddo!mnxYfXB&pF{0W{&CdxN0=Iuvxb4dz|Ts0>p} zzaFy~A(pc?ZII?+R^AQ#W%hnHC}x6h@7CuLs$}JGZ+TZB z$*l@%ZALd_;&|wfBR;dysLd0UAt6}O%!pbE-!B?gzY^X*39mvzO066Wy93%dNGq=f zmOz|)A|*txiliTub&^zK74tHbrP#g1pQ`+%@I=0=6>lJ7$>64a)Kp~0ntuxGH~n$d zBj;a)x!o8oy7^fY{e2;<=xU*?$|7f;;a=zDcp}>yKEKPR5 zyUdQ$JH}mB+dkXa3=^Y3nm+Upn=G|+C)C~*Ktn}Z6L0S*nJfOC?j7E&dKO({wLiye{M&q__c**xc9wN zShLq4HoxsG!szVH3CCh12c}B^#PI7P%`k_}at+X5|L5blAgp@}#0A_1-ag_|#D!!e zOUy)uWqVMymE8^6C(4%YQ z8isJ%W5u@=JmE#UKMp+q!jeuyv@EMPW4!dE**-U?xtaVmif$Sv8Fny*SXk zgyZEF=ZDx)j8fWR4D2O<;!Sy0D@%um4j_7D6HK>A@GyMk4uo zrR$p+I~e1^;W(OkQC+x`51MeOFkGmdARw@V~SJrd&8bhMvK}QE)B@d2aLR!_Smyum)n`EAYLG((JM!m%M+CTCJ zwO_St6V~+pYJ1$W&h;CxL*Z0hs?tvQ3p98Paa!zO_Nr+x zhA>@`Skg4mhvQdKM_+=voGI3bKIvN5Mbt`Hdx5nCiLpPSRD+pk`gW3NwhcMxNt-gk zjCk5Cjl{GS>Bwt029R`kZX%>-6sw&iw(N7V!N51IZ49nYuU|a5OLexd@>=B%!`OrG zBigxeVXC$Se#=aJl?ZN92xA{kQd=Og=7@S0@$m);Olo}HV{g$LP>YF+C!fz3CkK?) zR*#`0A$;pM;7loGG(>(zrSnJWFsJLB#=P{Bq;Q~!U*(kBLFDdha4K&s-^6$$&&hB7 zRyoK*i(zxfoN4eq+p#|jo_%`P8HCWm0gQkT7@?}oSXa^Hx&yI1%}wfxsP}b@c{?>T zRCJjMOO>yGdz73v0&$H7T4G-r z!?b*vI`&9576d6;PyGV@2`C=REZm$&_xXdVQpBKf6o+`YEk=h^z*09%yqK&thfm_4 z4G2QvdmPc_?LQd5Y~6MVd}QiqkLIYbSrXr`j<$p=Ry%9VNJfOVGkik5L~wtb?3X~{ z??uIc;GcmqTI-Bs`9IMgcCS$3c}pF+MCOyObfV*@?l;bbh<$(BdvgwYKZ}ZW*O5M>ve8(7(-8gp zgL18!JO)EQC|4+?j6up@@Yppo6h;#z?&x#x@{=9hcbO+Hn`BSl>~HqF2s?voIv&>i z8PV?sluULAIz3*ZZaBF*Olq(&&m!HD95CD19`QYcpYr_%Ng`&7Gc>KPaZ$pjR^*p6 zWwty!{0S3?J2qW&c$}duK3trCsgvmvr<;(@XvmCDBCQbxw00QmKq9R`EzbxqYNkiV zhO5M|!mt68DscYGXANna{(=pvQIiyfL-eZ;9;9+CgND@@uJ!q^IlrTD+JliQ8Z?gV zgaNb`zeRAqv{)Le%g(s*Rik9tQXxhl#Z~*aXsvP zZxhwbN_OV632R?yIq)N^Vq4Y{N2^R3P~tJ+k)ZMEd7B!E_rH294Lls^o!)FD%sJpz z&UPQ!{>U$jK=PMh|KMB5@biq?Hira|y8n~onSqFuOC7mWf@tLJFY)dq(Vb41)g^5L z#kCxIXBe%#Gx@+G$)9uR9oWiFzUX=M{NG zQn3s!F{S^Hy|)aDa&6m&2LusRKv1vnI3Yjf(v?BgvEehl%1LG z+|>2EGRh^WjhZ~+g%R@!O$Bzaqi6i8<@riDNj*V_5+ajL;bHeXTwx(3;U25XDETtoqLF^#y zCHtLv5z+7^4=q8?wq_LuwKu}bpysacq5^k3vYW=P^f!LLQ+wCX)lsNbO)8&^_`->4 zn(lsG<40a91Mq7~xv^BpButHk^nWKZ z6L*K#8juk&tdWAb94mwGk`j^38#8CYIMi&fJ$AJgx@pYeK_u^Vv;*FW(2}45?KmYp zwug3W_a6)3QUq)>qcbl85dqfM@0+AayYCA2z@GPh_YB!V9ED={3*NfESDx^;(Qkha z--X4R9v@;G*%UP|qH8I)PT)Q^`N0>e-YI#YaNICj-uO6nhX7|rVI!OO3R8?ziaBf^HT=)%#%r3`XP0PJd=Y8OBt=1mHm-;3s^(l{N?PTTZwx=zU9fQSdiZ<pffAX-grUA5yRH9S{+@P+s;2p^V1&3JveT8ztY;+rsM5Ya62g$vE`vh9Sr zuc@zDAD0}zHSJ18pts`(z3n9k0nb;r`oR4<(9uV-h)T>)Pi=g$Y+?QZ?Q+>F`RI)e zkFM9@+bofENM4p$b2F1cL`K>N|ARw)n-wG}%xzeb$`^L;d5D10aY@tgg5k2jOj}%vHatEDuScb^<&FyE`dwm%*q$($v^SR5~uZ1`4;#PvgNRi$NJ=` z&$H4_N~PV`LB3=)7n&1&+@~%bZbb1*u(rrlF^%F1{S(wu;e(?8Sl{19x ziUS`ze4;-%41<_^n#ay0Vvq%`4%9da_lf9Si=)P>2+H*(rNp$o&)82fAg%NEyUNV2 zO9=hn(vv4A%X}HzKlK)>7y=sk=oLq>9Qbs>NBxI3jF76uB6g9$&PTNBi>Qs@LT&-l`~%|4o--Qp0?_!a%N z0B5o1pU>d^(yUhK9Z?OI?I~~E$wTLttM6b^g6!S5H#vyugiRhAI)7bU-mdIZkvn+T zFnf8Q^;9=Cn;Mx>CnP?*q4uMQQ|e~LQ;oZsZ`^h9__Q8SQr+=^g=;kT5?ZN<^3!86 znh~;xJBRMOg>hZ#`vGRZvXb)g!yiiupicSS*Ve2xH7fM|hTdg((8`3b_x#q*^!JfO z3UJ@+AWo-Cx5SgLj&vYJR2aNxAjj;3L4)HSwkwr;v9uHHl?P0@i{Y~Ozu)8^$8W#w zB<0e?{KN-KpbGdhR%{kHOaCuVk-5iDktUCjkj z7$0rj;Y;)wn5dcc;g1%_U{_KDE!(eI*3%Z5C{5tbHMQM|?t4!D1Z-n|cMh+kPvdsy zeI{O={XkSw-)#akIY2-=!h07oQ=oipW>i-sIumAP{KUI?bxb)s^`29s_v5S+wIzrB zT=J@TS5d_FvFlX>e&SkMgZzqk8{($KD>l3;120F;RZI)3pY&^Iv=F*fxa5gwK%e{w zh(vt;q~=sN7mFH`DZ;dMoi((JsGlDsDfXVEiJ-b8NDNTc1+(qZ2o@{MofnWF6olrP zO(7Wt%?rT78txXwCHC|*I-yrL`=O(OB^c;I80_V9sv0GEO_I*F8FdO`ceXIbAPY!L zT1F-3>AY>JWIx+!t1&nAtjwL4HID?zsOgN82)`YfhhLq z=?}lvs-HrT^{?vF`*n{#*T-Dv5YE1Nf%$hq(;N4t(bVJgS&~V~*_?{C%~T(@BZe{@ zFa4m!>+jFr^F$i}Cs)22(4lz3{1`!I!=Kq>h2Qp^Uv2I^GSz*KvAf_qNq%?jy0Qo3 zt1n~z_!Xzs4C+C*Weou{)1}Z3WjPQL3AOA$etch^Vq+ZBb62W=T_G~_Apvcl`}1}p z5>+1%xev{ouyKWdUr`4AX8h-?Pv4hDav==FNkkS8mj6eir&ms2{_ht5@5}%1wESOb z`Tw9q%vXs2(!yPNW@8o_y0VMrk%hy;IUCy5ijf?)y@cqGl{rTGPI9}wM{Ae ztfvSkB}B&U7l{bCWx~uon{qX(8H(}ZZRIPPp;@r|3-IQ30}UEB6}KfbjJ>1*ck*3w z4=MoU0NC)w(adI=BaYtj6JKEVk=-}%l9}u5CnSKv3_aEnj~4V2?G8xpdl;c)7`L;d z^q$FFLs9CO-COTf&wj54YS!qO_om8A<)c<#IBL^EugZk?qUa0@wPsRBV6axtox!Vk ziQ>=sFHcCK2%gW339%yt3}di#tAzF)PM|#yaqSgmqOe=SrXN5+`@ai(q=uyJORnUP z#xO8mbSNoffO>nJ;QEw|;zAPY-%WT*Ieu%;J)j>s z-r)^T-EinUUV0SX_xe@0&5l0gb)^GQN#nuhD2q~W!LwX^Jqw&sX16-V&F`)$(|Tf` zvYDW^<+wi<3Xz<&T~CPZabrPFtJOOk(u9weB)cz>s6Wrr=t_uBXAtdibjo$-VtlR2 zaE7)4831Tzyc^0B1UgQwL|k1F6eh>AWUrB+gE0&bOtDAnVV79Gc$v6ZqI{tWD822f zhhKkhe|^cjS5j#SjuE_vB?>3yGEOl)jB3}caX7I!&`<}NyozBV>1=0|mt? ziQYs#u=$~&p_8EfE-lDt@7zKSdg#*Wax*uQ0;KYAC*AdQu}>~Wjihuy2N&DSKYL~7 zVt+O~ZunF;IfLaN=q_ngM;jgUn+C>PX!O;2&liR4_f5E#K~U6SURL8LV)`pvJ4SBn z2NJ#l+O0D7)s>7&h^5aG#g1j7+`FF$b`ou^huE@!;h9_5LOdfpeOP^PrhrvgFQUAE zMi55-*WzDT)b=q1Ig+<^{b9g>o^q-J&P%{Re@5|{l>Fldc!WfjckbPdQ+YiVn*aPL zr~ntV#Epio{mTaVnoxv++SzXNJ^q)yO5-F4ESaiOU>(bUUh?`_1De3?8+mTC_m^t=M!`VFlqaojnyMQ(3WZ+p3_-Ai{*U9n%i?K)( z5_{)gtv`(dsQ=aMOa<_je;MEQY(VHte(VVRyY+yGQ)zUr%;WyYjGT43_9~!YY7b36 z|GRZ98fK!WQ6Y3L3jgTsnUKT+bN~PEq5!VVlK-{L=rZ0`HFzEng81($pUztuH8T-4 z-eHs|6+@W;lDZnB5ssrx%8Ihcd!jJVYtptc+Voc z^&+JDXf0k|rO%k+&+dYVv23yzeQ@(#_4J7pIT@)T{5n0At6Z><#l@)!L)t&@mQ|q# z2FI}t+6#{7kx?DH6r*pEpJhYu^WYVzx#N2}Kq>zrEwcQlQ^7>mAJ=LbYZn<@-W_zU z7h22!vIhR!_Sxv4_kxI99swFM$JjddkkESjxrm}M=!q!PsvXF4e_!Sc9q5zin=$b} zdh;}i|o6sDYX@uF2oXq%KZ$fZ)1gZFunKB2F9j##J;U;yj#ad~=Vnqp zt{eU?Gyg8t82^0yT25&44G0l+I;pI^wfp5iA8Tp^mTFUtjj$akj0>#-{q7E57K*=r zt4B4G-F8v1V+2-awFL7XBmVO-S$<%yo_-^H=o;X#cy2E(D%@|9)jO*)YeO^qN6}4V zr9TCmQ-e4Nm=>kMxGR>m-I3-MBX(gq0>)dzHXyD)W)+Z)#c&W0X!nAnjDBPqxsOq~ zscRR?(l-k^PSfuXdWAZNc3Nt<{V$w3^=`0?oJ%6W9EBNMWCjTzs$Rs2>68$Rfh+%~ z$py50v)_4iA5xCoyiLy$ciY=K$6IGQV$>qQE1%q_RO9^rV)x9NBoAA=P3<6^xvE6888>L{{f58iA6oo7n6T zp(n&Z9TEMtKcO`s2|%Nxhvwut#U?z7o4uAf`+ds398jy{F_A5xuLSzNO8oQz*_Wb!4hcleCdXWMchmg#=BxWerQ^fd zse$z-Pr?WZ^_x27s)})90{xWzBu!tQoV!k@6mpN*r^)Q%N8uN1xuQr_uuayENkvoC zftlsa_*Q5XD&01dU&EpMj$^J~QGWi7^~|cq-XyNW^<5js*Us{Eh!HsMmTEY4pN(@- zb^74KyH6)+!i|$p4nqs3o&0KtLkF6g0b7|+D`$HgtRmVaWFPTa+jzlloDL^7$n>{p zF6sfgYro`pSZ8q2$AE>oqln$SSn#oVm|ZM(Nvfld7i4?z3V)&Yfm`C-vr~{HqDW%- z(y1lxX!F>i$gRFTLC4LjehG4=eoUeRYuda4*_b#-Q7TPv*5OlzAqJnJ6~bA6Zpgca zc6mK`m6gJ4n!^$BGn3nRUsJzh@1katB&*eWK4KHfx;J(S7gLe3hh`OBf2*Dl@!)>{ zwuu{DlKt%o^pf|>k;G#b#O-iN7of4v5uAd8y#zD`a%^| z?_AiNI{4B4Q~X?0uK4BI7R$7u$ipS+W7ULC?oUreMf?F(JYgjoYz`6V|I9{ z$u*R_U(mn>@~!k!6P^_hl!a*QG*XpeNI5XMgGAitwMkx8=ky&sO!(5hrYJFhTQ))H zhEJr{dKfnGm2u;fj_C7$Dp>$vW^q$=3xc&(&tbWp*F(Te$s+)lM z(a(ifN|E|Asz9OaLkA)yfsb0gq`w>S0gHOs5v8@8J=8}_xL@BI(x*lzwtHe)^76{R zz!)HXZ-T{Q-(Zi+;WQwDpZ7(iP=T)1?!%SgHoHEigc<~-=3A=q;?f^b2jGGc(B3D{ zBihO3nuIa|CD$&4waf}lzM8=*HrbD-=JR%S$RFh4DhH7fkUr`6YS|A{7tqJE7VQra z)>f%~cCYzU2v^Sxb|7EoT zqEFFLDv6&UHlQQkK_g*^XHz#XxHNnPbYhtQzwH)qR4<8;re3S6BTpD`;==6eH-c1l zQC252glKlj1JXeK?|FisnqE@vra*pzynC0V&35?OvZB)`T<7#{bx<=S&#Y>h*na`| zZ_wpvx)1u~^aMNulNf|FmblB`YrkGEkLmQ)?uhBX0{WZW{}13U#)C%V_JVeqia3|0 zyWp>5OHfLgz`cNb3s4RLEyaY;EJU)Meax37Po$hN=)r~O ze*?om9=LiDFwi@1uRq=`tBe#fw1Yk9$qJCXdk1pv|CQZBi`lhq#$}?^%ln2Ra$J+( zI-^#?nR!3wAW<;Ly2^`gx#zf_vqE(sxf zlW$i(`nsy&DDP&~zdRXEN@8as&dJQ{wnSl^scP@dZHUuyazL+*S{zLTS0#EHe&)HX522M=YVqonnQ6Z{@6A2PNU@f0ex`_{Vgfx(a~z^Yq~t zVPFj8A`V}=z`IO0I5j)VJa!3XaCuEaJZajSkjut@x!muLe&$`|HR@5Cu)Gbb`t}lB$$ts~9wpdk~qFom_g?Sud}IA+Jj& zVO(4^(0a7+l6r@BK!B{Fdgg|FU3E0%4q-sdMFwpu8oIvQhGxyrk>9jeGrYTCUM++>a6dUCm0@kf%NByV#?MtIS}U0zS@}PCExr@13RZ}y z$+@pG#P=ZGBPTp)H2CuYQ)b0t8I-Y@SBnNm>skM+IE<2o%**UJtsN9j8SCt2F*?Yy zq%9a-CE#Y6L~r_bdCH1%9KV0`lT)lL*vVKVR( z$iv8p54#4nb$Yk;KdB{B$rBc1yoVBb1zt~M5A}vu;TsSk8QVagy>8NSaLr2Zz$1Sq zB2nvNTvOw=*&J2RC7v2lKj+%|jmkyc@Qb_+dBf;jd75oCuYr+)*9h+I-?cYozJw+K zV#|N^p=qbJF*i9AlXQnEN2GuQvg~2edpkl|xaFW`Y(UGK(7++ya3BTo)1qrkD4s9; zH$!+wx!%A}gnW!mCSZPDm4#YWr1H-Ly?(k$(z+3i>-2bUCgKc48wS=YieE(}2HtOY zHRoor;@91UUrXMlUtJhyH#QxD=aRd7!AVW%DLn(>s=Qal94ED&4ZyCfd1;S+?ugv~ zCVUY*&oj)IG4pvwRH|ydD*Hxi!H&y?e88u>D(GvJ6KuXQGj)D$ZQ{nPltYZDu7uUF zi|2T0910SSWB?o?r}me@%CF$6!TXgc^Vlrq9Q5%_7F?sC{cX7^pAa1QPLy;$kUIkw z(Tmm(N;h$L`=V22yKlu;-X3?u#lrFx(}u^IN26PqCES3fZLAMz2bfl3?b`CfV~9oJ z$R}Q+V^>-HNC547cEU8vHU~bowuP=sto<=P{ z1TSb|T!heT*cy?h$TOG3k*&{!FcCAXKYjO>MP^EmZlq|HTGO#TxNI-gq$q(Ng#Z8o zCwjtR>*sx-?8=w0^peRlLIgcZ;$w6^iE;VK_#DCq7>&CD zv}Vg7lE9A^6nlDiZtNr7u21zn$iP~0)E0}9zL=$vk)YK551S~*h(?c(m!dDljmFNq=0VlVs~D4#x>z+T-?K5ztr(gKO-V45(cJq=Sx9zDH)eZD?1YF)=&4#j$kQp{YqJ2qVW>FRrmqa$6S2pVKw^tI|)hdV2mrl47j zxh}(+ohp3`BW<^Aj5u|3@PXc)G3KIZ24EDN)xZZ)jm$_gzuTVJx8Or-9T@g29C^Fs7EB9iVV$8UBcsx`cu@A- z8?dHLGgp2SFzjnH8;|#O%5oUkSK2+bJ208VM@t3FZ;TC2VN?xYgS@=vl6lMJb;g0X z?J^|;`+~Ro<3~Wzn!QPZ*$Zo;-^M&Kr#el1wX|kpc`R}o8~ z4qY{O?uTBUhPU6BmMY;f{&9YnMgl%%qU*Cl(Z(if#LUu^Gyr$; zjo_g++&maHFVemB47afLAT zIbDK|fX=PGO!N~`5JmNHi7$pvf2a1X6?1hh$7$pS(>Rs5YfI~|9OM?ML-nRY9Y$V+ zVgQ}LmQzz)>kEIhZsQClb*$j|D|-{>Ml7xkd=G&5d1w)zuz8SkWv&Wa|D02%0`iR( zh0dv8l}{ibMXDnqIkZnLIzChDip9(yJ?sHZ||J)8OxBMzKGu8t@3B*dt>43m3gcfglZA%0e;xOQPNP?js% zYj}Hy5HER;eu4*mCq8FIXhB})Ru$ma3tXMP4Di7U!VJ$XF)(7)B-8ArYS$Cb9Lgvy zHJJJq9qjh2R?v|d7gP~ITnc06ex?a!P1)2v#>0A&p@y90_RV16D`2x?8`aO~+~;Pk z^hX))l&^f(RavE0C%M}x?@a_I_NtcQEn_EG*eOq}i8fna+Ya%{D}LN-T8U$DvDe<4 z8DC$QT5=H;zn=LkDe~Sq!aEnqvl#YMW#W_+#Oi&JIBJH@v$7g=p-X9k?zv~B2$M?B zqp#NzT>$-@Unrt3*WWmbld!1reo|r;HS%2`q~BICJ99K7j+9wC8gJIcp+`*?eX4^lD@V0M>4uwp?(-)HVOoTWh2lvELCbxYt*8TvkI<0wmmC3`g7 zSydKxv&n29#)aF~57SJK znHHxY#RH@4_m(E$Bi~t09VFSw3vqX)@(+zcLBk)nb!+bqfqlt2h9XV2OoC@_*V2)oJrQ1E8bk|16QL3 z;e~u+JNw1&Z(YFVnQ!bbBii4=OX|Ln_;fqgU$G^pUh#b9^;HP~z7UHH(WYOc_Mion zz_^7&b+U6~ZzsEYvvR1830|$7NHb>(gsPK2BIEbeE?0~ zltRNcU-RoO^{{V<`Smi7f5vX$JXGGa*%7{}L>$!mxy5xa`AbboJZCH?=A$ z)jXNK7x>RN5tMgW0YRbOCN481KL4(!^G&$X;cz3LPRW}?dpB2j(K&fjkHh23F7$y} zGEsVba*2(J(Uu>c3@7jIn+biQ-j;~#!hG7ln4Z#(WJgmYI$*~-orqiG>FRhUF>H5{ zTfk7UQLeis#Nu6ReV0ida*RvDW;a#M_AFy~HAhxirv`e_r)G)NGYj5rx(?@zJuylb zozxshO#h@Jy$8hKWorp!mb}~e^p1CiKS0%v9qA~HlNB^WQ7FpPtq?Hq-2?eiK|iNr}_H;dqg7W z5cL%Z;fg?WUqtF|Y0|cvx3y@_&;6)7ENyNOLwh$T7lW6h>(?K$NSGI|G6x79&zQY6 z4Fw?PYKV`>8zeOY$s0cgmK)Pc)12z(8f?I zs?vfjQ9A(MR^|L&pU&>4e|_CZ@Xyl|e+9N4}aypuA>OZ3=d ze>!LIJn!1KcaLjpSqktKyXEEL@MQk<+TA?+A9NMxxAD<1SPkagO6LK~Wj==?@mI;p zhxDG%Do9$Lkc#*WR-@#^Y$e$yq@M%x;oNTRO5|T+!xhH){kW^Xy6$5GElR?aH9mdK zK3og~t48M2NK$CbG3o)H)CSYW@Hg&UJ#@Zn5txf%(+VA=Y=t5qOqF`}O9&-6m-e6^ zMcjV{6vwEDwoOVZR{b54Q(QEA2fl+m;crHajkmA3FQH=_^W8F(6%{?c^gT|v5ci6% zld3K~W15Y5WDyZ9rGcPOtwk@TppE>Kj&X(uHTVq5xw@i?x>frW)D#>22HrmEviL01 zSwTsf-^H;5xr?Ag7X1m5lElM`*zBKm)0~kmZHy7zQ(M+@a6z|H1%~IVmA-O$s zOCODmP}9P4=J>*6se9H;M-J;j1*Xqd;5FNwO3*=-K?BOzeaRv@-{NlGSc`u4H1HBL zApoz!jf7}GpB-X<=`n2?6;rCXe#SjHtZB64S9(26Np3`O)tYVm`WY;zT#Or;=rpPK z8s|XSmQUilQZ^v5YMa;U(yKAB0OWIFdY2#is&c2n4`tvSO1(<4EZ$#l+=_m=i(`L!*&su)liHvw1KK?r8K%=mCIKFDKY5%Hv? z4mnNFwWl1PE2(a`8cn#^LtY2lIB1?>LZ$x24xGSWOqXwmELtA|j15 z>-C3GSrR)nx2AS&TVAa!!1zw*nm<6;-T#=cmpAp<3}fDn@)zhw*f&{8NgRg;1Yp0i z3%`i1<=jwo(OJF3Vz#U@ZG%yRL<2YO7_}jk=T<3$@$OZJQ;mOp+rGrtTRf`egb>N} zR#s9Mq3x-(u6sC5OH@Wn7=)17+|fo?miKz-Z$P~Vu$Uq<{~_-z)8XzC7lgwBM#1=e z-=YlP%(KFa0Bra%`Dz!F$~qWN{JIz#y7&=^EImMa1P_Xni0h^{j1DXii_tvCjmpri^AB37zkcQyPpd4K55X zuahC`RLAR>vpH551_Gti=gIcT|B8jTC^s>6S7kt< zRWe4lQOk56BpdQ6z)7$IezM;qwO_0t%KfH_^0($h20BGqZ#PWk?n#9n13awE3ut&&n#gWM!2 z#0T{A5>0td)l~(%k{`Xc`La7D53Ti3#|aRIV?gBThQQ@3{;OBrcXUqpJjAn#-~&L+ zfLiRG{)d)$s*Pv@tA_=|Q|oYJzI=QA0W9OE?7@R`RTFsVUM-zW+i8>vMC{H(J)jnk zpE>5(>?oJo^*CDiZS7elWfU@qwU02M_Zjnt$iN=XeU7&hoD2@bc>sf)QZ{%@-Xh7JJMblGn>>>F(z>E0+eR1=Z*@2ac^iF%^2>S*(;YS7i z6HdbW_K&d$T=_bE-A}$%m*|#+3whiJ6i#6V^%|P1RUL@ST^EY{E4PL$Jp<4O4pRGB z3lT2&&mm={!%~c9GzPRzD&3S-6=X*DmCiD(vOCvG57%=)q=LRp=#_r?yiFn6b>q3) zJG@0tphXTrg`TI34n$UK@rdZO28!9bCf*=2#(*A#I=I1udG?EMx@TH5e0vGEskfU{ z#FIc(!Pg1KGiyyW3{=thNr&G$o2vPHzv010-m6_E+OFZh$XTTnlaD(`RDgiWN;agc zo- zuWehh3akmw>6(0k)t}Zy=)b8+nI_9F3q17PRx*v!8h^F<2=^l@=TM_&M#&iTk*!VY z%c2KvM9yjQ=8U#mdH;Ds4=Pzyh2}q0ZvK%cdY?$prQew;wcEb#qk7L*B{* zO~$twnR@Nz)LC-kE!WfO$+W5X7SDFy&)$#ys_A;udP&Popwu=_?~s|U)7fC+l=zvVAI{+_*VygFehYYemAV_nShLJB=d+bDMjto5 z675h_83@fdKx+d#k9OPiVsq@M#ny1HE8BKix9pflm7RjSEUGF;lp_gV4=D<1PQzNn z#M#c2KIiuLUWU1aIrn%-!BrxDhb2G&<=<}g1 zV_fi!bFXCl?tb3hcPww~uy)sp*wm<1ZPP7={OTJvU)-xoo*9W^I}@=V*Zjb5x1Spa zqVxGg>n1b&T2nSWQmZWj+B-?Pb7BW+cMmuU>odEP0XTcLL{#J`uhUwl8Q(DWL4I_R zV7WcxSg)KMR2OR{a#d{aO9i`~yV8b>UhN@}rB&6m-R9I$5?vj|#x{&S*f`5jaYhiU zh<`S`hBL|AaH%{rr*lvmv0ze=$iF|-}UabQ8`D0bq{aif*PKG401-paCmT6Z3_ zOm)eLTH?tUwbYHZm8Uf!QfA}g`Wt2QVe4zG&Vr+37hPl@_4|_32e_)AgB8uL%^=e4Dxk^*?a&hxfnY z0ZqFMe`7Wgot#P7e1>+%*1|gkEi+XjUI`PGs;ADmo%{R!Pke=4we~xqpADHkS_oNc zNS>MFD!}AeChxVHyg@_)s+YMVDu?+9e;GN0Gc#SjF@u5}0I9rjnM7v+emntc8^g@Xv8E+5g{fwf)SBh@3@TYaaB_(`m2Z~= zpMn!7YuZE8IlMRF*iIN30DGE7fZiyd(zCRW6!MX6)zMO&nj}%!uP5(+L&P!oIe44h zS_zmKQxd0-e;gisX!osppUgvA`u={IWQO(}Y>cf*`1tU=$EYobP<~ZD@C3`0fPRa$}Oz^U~88J3u-gcP&hjWr=sV+ zUC%-3x^tOc2YeUUu(vDmxAx3{k82h_)T8cfy}*5?_>F3{?7murTY@WjAMNkY>bI&iM%Q9~DX-2!+@CB48=GRk74xp> zb~j2pXQiadyhw>wpg`*nc3?Pm@q_j?qU~2$RD#uJHW|Kd;Og2cjHd@slB1kb@Z^*u zhnmm!hsMj)0rLWWP&&`WGK1u*4ls4#@36G0RFkz} zh~Qe$9Md+>r99*i$v_?TUa{#%9qGF!=Wq0^8oc_Vn2gz`MLt{TtGu2e+gy^T8}Wsm#$$=pt0NepYLT3){vGs1iuYj3$CH)KyN=U;W{ zP2k4{I=^JU!D&>Z4?uL)KSO$k1u3ECvk zY9=h0xLdEHYdu#L`&eVEaX}14^QP^6*S(Od$1esBHwuOZ+T7x&fNjZB`uFR>C3X9B zVq5rSh>r8k?`S0#Nzb+qn1)Z7@l1U@%EWlYS*uwaBUV|E^}IawOcDqNY41fgMnB`x zlt?X0c|~(*(oR-#5}oa?%w&<~m&o9lL2yv=?_S5QGDD*O8O@Bb+5MjfuD_i(L_7Dp zf`Wqf4bi^8C)z;{{zc(<*OyND_j#7&ZsbY5KH+|FmRltvLGw_fuBN@hJ)?zS%wA}G z5_Pig&p8?PWU8|H)V?lOpuv}$iZNESgEs7|+mm9d3KZ88It(!`8{HjNMdLy`S@Yx9 zeu{~%M-xQEI|<$xNpQN zc)pq+v}cJyU>h)Hm}5>5)HQ1YBEVnJH4-bDt|M6@%5C@n?0a6InXy}Wnn zFS892!O`=*13MphZJk65gq~$w9uhIs7Y0Czm{_M+t2Qc6WA-3r<)6Bhf)TV5nq*na4Y6S_|A3V~El?k=SsQC)flM z;kIY~O0QBMSrL=1bEuIfL#oZ*rFio$p~0i5?tQ)G+w_IaP}EDi`<0FMOE{Ats9tR< z`7uy@%*^!muxx!S_G{4=e{vj`kzQ@yWnLVOefX4MMS!SPTP&K^optUF+%|raWg<(_ z@vbqMdB$Irala2qUUKY<$E6a8jeGS(rU)z9!LG%7-9xx!vk?SudTC6@*?sze+AO5gmN_S9E_zRm}B&uAYr2pw(BGjJjqegOqnb(Fo z!2ScTTu+Uauq7-J-gZEcaWsUyVA@z*^5oeFvCTY?%@+PX0ogpMKo~=ZATAiFeR3>8 z-FJ-WC{c68#{KTBfSEY4)6?mT?+>hu&|Menb#xjJpkr66aS%682Ujs#eyg}&fTFRf zRb2!9wO*$|VgNj-IlX-4{Zn)%B2l&C(0FsxxVKesm$rPv%YwJYc*d(tNWooJ68r`u zzSbXhg920kJMf0l^;aBt+V@z9-OZqK``XWeRCYJYcYe0J8&e%p?l#6Q76#;H@bjC#(5xKfE;%20pHFVby>HJdhpnm;UG5HB%a1c^yO9=+^pCQPJ1ldhtz z%(q-_)c*xPL;U~z$of*~ijpbhh9uv*!S$*lWx{13WI8-jWG2n!ow0qyc{%Xr72fIF z@;5f;2_dQrTP%yY0EIdk!r|&_zSXyWi(8sslH0d>jt)fCR{hB7`dO2G*i*~A^04Hv zx+d~`Ofh|3rTy>UDa>!qDw+NwQ>Rg*ud){T_r$9axG~CdK6A>+qmw#Y)($J`>j*RrL3+V7GR7o3H<0q2ax*SCRizEOdhH>wJ<~0FXFfp_3V%b za!sS_olK_EL?(LU;MLd7LUFZ`)lB~gYMk(H+(qWndAk1y)~R$+sHZgw2~0%FNCMRVtkNJ`!u<{iXKXu)-thQwP>+R>lDLbr$Lw z6KHEcEip8v_m&sXnI!EwVXD)3(Tw!3c#@63iHA6c&-F0K%aH;(L^d1}^-cPLxV~lO zSgy$luivzqTol@PX8Ij;vFV_V(Wr5)vN?2M^sY(#Q>1eVv#Wn{o3oF`Ss38&X$BFK zaz9nPg}3eXMz&6~iw}&ah~<}zf8R*l>QphcMf{SylNY4!^0utN#O^nOcFK|Azi4|n z@x(46uxAS&_}+So?;|eMn77_;@qN(mG#C1s+rvAv13lU>%1^Y~7CN6i1mqrxIF(eD z`<+sy{}HZ_vfetH^&EjxEfoT6PTe6B#-YJ?OT<{~)=J=}a@U+4o2>nXk8A98osc}= z%%okzpUR$=lU(p9>1kH1oxC>bkm}06Iqyu5EG(=(DorxhvG0OP?AeuceeTYxiNt)N z8xj9W8Cf+T@Q$wL9}NTf0$&QCM$Ba&R-&7Q@a94dM~emKe)cTN-&~*Nedn=7G6Q9t z+;*e$i}B}$uCt{30Au zAHDaa;9b7m@A_Y3G4riM&Ku1CGv3p`NLij!Myq8fQQ{As^;p;Hj%CzFdg^&sQXpOC zAw%JbmGzF5V_!_{w(ng_q{I=|ByIh-annTqV8>(oHwf=v3O(t^Ep=FUy-rt_{N_Tu zg)6*rk9vD9+O~kny>IcO+2;BDyj((+I)h5|)$`{r4gDc%zQi=v86P5TiJ9RV69=ws zUpHbKcJ{{SJheT~8`n6H^=~Z1yc$fKxq82glb+V(?$s~=ONig&KlC&Um?lmx*guf% z4JMtfb?1jujha@x)-~@NSo{p8Vu~B{)*SCk_a{>s4Pd#rX?OnI$vN=WY1zYHxD8}H z8?NQ0#j@bO6FkOg_a)7BR6j6)S*2pZweFJ`9oEYOU|N`iXKBA#E@9*5edO4fRa?EP zO9!&EtQrA0?Z5RO9b=DTzptaJcw3iSyaWu;o%L85TzAg7&vdoS#N2c)ibd7KG3P=1wRr{rn-$Oj@D+PMNb2UsPhi#S;v~F{P-(r#PX@PR(HzU{y?ZV!!kF#=~ z!GQ}OR%;stO!J3p_qj~vE}B!H7~;Ak_A2548Yho5c=QRrPoSas8NQz(pTi)k`ucR z^3YNk0Hl`a!*-Qe?2U@zZA1~ZqdVOZAkxxU-oStAVwfoLwIvX_%?v@K+TYaU#G~y7661cEuGqw>ZTCj_ zHBXI;BJ)MA{Z(3MJ(4ch+=6C7-U0t{xvS2=Wc1A=rc4Wa0~6UFkC0Izm8szxydh3I zweeO@UZ+gGe3mT&(cnqmZ)z>+VvHR#x+hGxvC=2--sx?THTD zR{myd2xus~RhrTGuS$-WPw44-a3u0+drgi~Em=+B#Sy-G21u8;&0H~+>M@A^4!GQY zdJtWR-bxQSvh%H_leNiwt*p25p2^tw-+Hl`7{I5ZNJ5$S$xe71@BVRdZwf*V74%t< zol{+Cqn}sY45#+~Em99`_VmHCNQ=^Mb;CoYgacFkO=r`!k%vbr^u9M;%WQ_~mbW~Yp>>H(#JvPigg4Oh)0qxl z2dSDGgev~Ar-;=G_rOPTVP}U@t;4p0lA$i<7r#uczZSw!20|dwzwE^~Jw(?uH(u{8 zV4Lr@ip#~FftsD4pE4*F)sLRPJzMajwo`9|c8olBQinjWHN;Ynd)&R zXXCo4pnJ0^iwAh~RNLBqiOPEsTJ)5b~`kE_E-qEU#C0)oh&VFi z^$+ed7d$~;&z4NWcS~~mKd8i81|>vk{>tXi-2mr*wEYAKxHoyAjGf=1a-7vgIU0`q zikgbK1KD2vaV-b5pB_D{xDK341x??TXKlKpXcY{WU-fDubwx$rLHZmv&ER5r_m^03yA0Ez z6z}xF{Mf9st~HK$w01$SK~uJ#Rf_fkMeue#&hz-JQnVY7 zR$~|GfPcFCMvw)oWc^`Mv#X76VT(rP z`}8zFW{-o*XKJ8S?OWlEf3eyp>6|E>`{V-b2rG*yKT98oj$d&j_I27-ijZIHssOF0 zPH$#%$PU_&VHNcvn#hBDDpz~S14m!|Jo7W zXCxZTTVU$ah%pm;?d6~XdB+AM?ekJu6hy|s1sts~Rxxr$wamxq30fp%!@)Jxyr$kx zz%ebb!mA*Er96u=v`pLxZ^OpA$AJ0V>#eLD4MILqz4P%4ew~g@-Oj0v(>B4?1Caj& z8g}M0DAKg^bdQ(B+!!rWzgl+D8hqquT*JFBxxxM{UoWC`^1<=nePTvyjIQmw3nrT< zzd*M%`BQlw+in<2Puq%!GexyXVnKtc>dJFQSV%xE$}*E)Qij_uXvME_1?T{ zg@d3d5CyCo$Q~qh7|Mo&ou|be{>Molw!hA9X6L8Ob`a$5-c|vG_)Mb4LrZ~o)uotb zZdcK-yu=AEbV&K*(1VfC^#7$)!s=c6_H%Odae}TO?(0>(`OWjKTlcV>g4A9@0 zapy=y)d%MQN6L3ni|7-E#NwpK>jv-)n>{?kW&(Fm7-6(0J4+sZwy@nbn=JB+F*|~H zQu-u;pX*ytL@BP`{+Rul95B0uZ z?vQV({1XEhX^<`pa>2Rd2M*|qGkK#}ke~g;6b$ufF8sWAteDblQz`=qi#e(&2w~ z!8^lyL~{5YKV#oltvMgtlc1w>_?-p*tEnnYU3Xoc75W(kQSrY-Ap5R}WDR;vZn4}y%B24SvFQGV@A`2Pitqzvyx)wq>*r$1 zz4a7o$)e_60%UjyIpgf;X$<($^d|hrc2tbRt@%J~Zaf*voWM-+C(DPhE z5(47C>{6 znLxY>gy&G78StbJ53ZyRFgpL0QLn@ut4fsf018rxlc!squN(8_L0bJWwQx-pxk;)P zZ>Ji!8lCPW)b4!UJwcuq104*8jd>S2gaDS6c6qCoa!K%gmLE4i1411#iupfP9-et8 zlQskY1XeXBvkClcNQP7CsEqfmGX=Irv)Eem^9=Ks)#Tjoe2VlsXZwzWcLfQ=+vfXN zf*Ikp3pg!#9ta46b28#0>W*=EPJfj-ABI18`7xm29whirtY;GR{<0@oLgQ!`2c)#W=zkAF7*;F`xgI);9Y3oeitz3ukjY2Pwc#Os0AHf;oL8KFPAnj!qM z>waABZ-WDcCr}hpkxd?|CiL>Cu@W_)i9s6&NZzZh6#$b9+A3(Uu~RJ`tV!MRb6p3g zZ@b;(4fkamXbDa?j^x!s6YOj{KQ-6+Enbm8G7(u3k^Y5a@TS0lR388A=U;Jghak=V zkXshZ(U*|(y~Db(41T6b(}U#h$}i94y)d|I4+}Dd8uO`ghF`goWMkyP9Z#Ckzs3RG zBHjh>ckCU3--c(UvyBV9*T#8y*c@v6JqMfE&(G1imgK8$Fm$$x#*GJ+lK?>SXE zGD(otddHwP!S%g2#LA-MM=rf^&cE2Mgdw$x?Crg=uC$M9z^|*hCSOzZ2P2;yL@SB7 zj>ED}lcSen2^kW2s48?N#$6!+l{tB@RZxhKQ)VgfXa z(m*yNX-9sVL?JIbu6O4_7WC*uzh@a-;*GSQ^sc7>R)MmMFs}zrU4d$4(WOgCaPvRe zyu+6^iw&Ry?a3R*28ic+E$bIV_Si{M2v{quPbvXhS<9hN(^br=p%rD5^G{?5`R5p53kobQ-RG`%n&g zpuWy>!$D{&o>z=eqI{QjL~RS9OTu(i`VjgQ-h0-3O_+mheEb{@@(2s2o8SsEe3Ldx zprC-KunvTYTh!3<4rg!_O4^QoF@gGCZ_$*_9-B9KmG`Wl99)ur$&Rg`V|4{96OJ$@ z*0!|J34c44MuM}}|6cb5fsw7YmUWQZCqc_}K02rA+TsV6{zEcDpS8nNoEM!9v8LDf zawa6ty)It$n-$|>5>xCEccX~LJ9y?uomgFCkAVY1v~CD6PT&mt>MnDHJ164sqiOM5 zv84!x8airxw>^h&U)&LZ>c!^N=?5bR`_H2JM7jOheYPELzcyN7Uf_muPf%I4_aar3 zt4zT7dK`*0JRI}?)|EHX>#nKm@=E5S=~1&rr~{8L?^9+2JFcQH*M3f(XSblOCkOu( zrW=e%S+o>M)`Y&mXUejJ5FoRlg|X|mCFzyjMjyca)S9t*w)PrhgixTo z)Y`t%y0kcVewo&1REO7Snsze3d-f<6$|H1l(j(@g#N?Ode~kyd;1JY!6Q2%dL5h}w z2Pytu^Y<9d^zDb2sNH4HZt0tTw%v;7UQIP)MXP@6gT<{-j(pGiUKN)OiQRD$-?tzU zq4hrq;d6Kb5vd4I>NO))dA2VTYGd(@iyZ;?-ToUOK(br3AcClNK4GBL7$1_$om^+e zr-|jsyqc;bphZ`Vb}%P8CGSanR_^r^KeO9s&m_mhI!QlAq3!PoxxKXi&^!c8m^%t{ zi7e>0QdRBHbk+BKb+BJ9>?U0*>+@Xc3K|jMe7*57jukz9=2mXA^9A%3t2VMvg*83} zlFTrkw3!meGe|~@HTUSi{5{h>79tzsyq15dV4KoISeao0>}e;@#&>}8vTQygUc5(4 zd;|2TgeO{OWPiT9@3}HIR@qXz`V(eGW}s(f`1sWN;N_|zsG&o2P-L6FNay86_YWDu z<#{uO+069JdmyFJs4C#NQl$QPJlHO1ubgh4AII+KMB?`F0_Xd^dLZu4O&01HuWI=|sl0gj}M? z!?!704;Z0;!BLPM`RWwY;{8*g1tBB5MtV{^5Z$Vq7?J;V=rbBHNjRw90Hv-+sUauscC7n*9j$L z2|lOnZ-ndu$mG;Bll-`xlPcN2DmNgXy*6~MC;eEs6IC-HNoV+6ZF`z5K|3wllmzUQ zb1ZBwvwonNK%qb=eN*!|my<6s;;G`!n^2pL2S-TYEe&{!gF&X96zh;VjfyeNTY@X% zd5w_IlmzmYY%zr*`E^CVw*Wc_=A-Akl2U$b#CcMt-P9U3>%FxGE}^$#)pFL*l5iJn zP^swNK{`AL5BwX1{z9WQh%9x1sEF4cXoVN949`bbt!0OKE$ui^d=E66d;-JrG~&%)R4=w41CTA4|M+jP1?1( z3>|KBb5qyROLOp|;cNa6!GZ(fS^zHgR$cJw=sS$U9H~;O8TttiVBUnX_IpZ7 zh7GqZ@QYOhrJx9xrVbZ+HX;)@28eLthZ6hEMVVX=GFe62$#mBF8{l%oo9lwfw+AMd zX{kcni^DreexX}vE@-j;ZF&Cl@Qj97UwmoN>hXR(o1e}Latqq|N%{f^Vejx-YnEz+ zPV+dAHiH=Ufn~Eq`HzNf(fYY-pC&|Mh|1~;u$#oZ(pd3FLuFrXc>|ipAX-FX3kZ!c zdYAvX+R;R?aU|qt<@4&TOVV|_^=l9@=zjPqtapb3`w%IUC5^jiZUgr!*52CS@xpGj zh4ItfENV@B-LW!>mZAM!sz8kl^>aT8@xM2GvV^kC0Cu=Y?JfWt6&tDRt-m12t&!>jVCxh3~D66?vCH3y!)wztEH6n zbQxTT%Qh#;4scOD*tFsGDmF60YvH@g5o*(CI&a@IyT()og{k_p3a|YSL;su{aB#yr z%Nmqax3I~sdb>w*o~6&<4LsOb9W(TRCW!Z4e-~(jd5Q_Dgv9tF**!!}II3h zBtNLbUY*@K#-B(MIK|*=(B>vQW*|seU7rJPc&Wx%F}pAzP4N35j_{+iXwxovCo<4; zNyO$N`yI#jN%e~PEt4Tt=dKEQnfw)kce9#OKAAql7?&@L-J?}W%5%3{h9?pDkx)qg zEonSFc#pI9ezp9-v-@RuWP0(eP&{up;CkV#cnR9U?EDY~d}k|URrq`5>)35y6sn<# z-2_toK~&|dHqT&dBp-z4v4_Gxc<-gPtdCiEl^Kgqo1p}7oPa#qT<>--e-BXku4lFh zoEC+Rb4@Uw^?jY^6U<2#dmC=qVZf%ItIay-;GqSzDwoi5pOyM*FZ9!HwL!$%_b=$595J(MC=UlNWUdb;hN5Hr|f!NWtFF7BKP1Jw?{O1dvUk% z#XaEax+v>DXZ)RM%=it^kM zfQ2U(oeYs50jJ)`_j=5yh|aE^k?T`%u5U!FFAG`UVy-qzjjK?QX;lD>vv>5abFd%{ z-%olksVUa4*1Om3cD?#mifvYnZTN*B;Ze?k70`I~J#q_|u=T&Rxf(zSR}SMc@c4`L za?A9BvPPTLR_yHon+`+&Mx*3u1$O&%vgi77K%MuuZ_*TRbVG3bXgU`)Jm0S$MD*`O zGEBrMLEbciV@gB>vg{s12LgQxkk`5%4?V6oZ|uWYnNr(N^Jv$_?BmubE^H#u!hX z%gyNjH=lqVpy&@kA{PqG)h8_SLPt!@s4FpOZ~MmNe9!nNrqlDDG*yDH+V|;r=I(-f5Lw(1D5`;vRDy8! zBd0}hX&L^-5kJ~^M0u8{rmn`vn#WL>j}{MTz_s|}VT1_1q&*`LEVv%g5SJ0d8l&$s zjJm3pY(P$GVnw9>&Nba@b~(`*rJ1BK<2Pi^zRdGeUv;h%tC zEe%+L_X%odEE%^D-og=Co)M z?JEUSDfAuvv?X!(D2-O8i<8FB=Ti(+ZAY7xt0sbMzjP@Cq_Z_R9H=;WEtAlCHEJ?? z{>K|2qbv(+YWc-uv20Z-0L{-f4zvs*1z&G3HiX8;d`jM5UY$Jjk#EnucXD`?&tk06 zt$(dMtpWR)j1MNLkF!RSb5z^vy#faPG|AbIs?Zc=qWcE7Hwe#oS0Ta@=H7DC2MDb6 zWMuEiI>fkM$f1~4Zh&)pt;{e*t{i@W1DoikMG_;aybT`j*|1n0XYLcoz^Qm?{xjhj z3@w-|9f~&RWh=DQH&V=Y|4EhS+b}$tUoC&@GysyyUn(8SX4z`OyzQNf zy+XG;p!yThQ!_F4Vak8I6Rou5bmD5t%=Bv z?LZp7{_a>7sG$HwE`M_B`jbjlex|1llKYt_f z?&||KQf8cx=yb4I*T_o@v){r6Nd?Itk-Q@_vC&1Y&}z)W-atk2{^SnqWY$cVYj4y$ z7O%K?m7GK#&BRo6$Ve@@oiO->l@7+KY+IbGTSf#7HY0CxQNbq+|F>Hz4WA=oT-^wX zb4*&8u4*hRs-3YCa9X`;@bYs`KOF*ooCI9%SI@ms!j;2KJ+FAqg&I~URVFUUz9VM6 zldOl2!n0N)$@QZg$kK}tl~IZ)xj?uL>+VYH{;Gc|^g&tLxaqIh1S=K86B=T>=Y`Af z;!N5u_stq6HvVU|2bMBs5yU&iN4NKVKxd~1U7|&QC~on) zkIU_Xwr6Wifb_&8-n-ubcD?EOchIMN{db4_7bFn|Ut|37DG>U`Xs8aMrtfMLOb@}# z^Qc2#E`(JCb`vp+8cIZcbPt0RXaWV_z@dxE#I#6|ZjAy_jvli5X5S=Q4THT^8W0it zGnmvz9;S^xVGlDT3*O;;!HJZ|TmC`=mk4`h>1yxXaVF|O-{lEeaP_4-Y!c$Fm>K6S zm);`={W7ZAQo~l1-NIKVo`c6O!H@b?n$qCyhuVM);DvDwlC1jQKRWYiGVk3&q#^`f=l#&0V zl*w?qCzOY_LkfGUGK}*K4dm5+MKJ&W&g{Mk!`LR^=PyX|X&UpEB}&L3{6218iTrYL zvw2!-5Fd;-M<9ncN|GaHsNw9*gWZJ#f9dv}sn zH$&XUMP2ErX_R`ZUC{^e3jCLi5f)y{rhU}l6h4|6L#2IW0(!+QXpY;>@D4SW@NA&R zYTr*9?D;^mxepu0{6FkeW|lRUCW1n!(vRtnM>5LS6(r=2ZSF$n>`6nn^va9 zfKVI~?#Eo~>oVNl?Kl*F03zs%KLD83;R6SWmr8|UF%Uz>Q=0biH_?4DzKm(1H}fBB z%v#2=#+StNJH&e`I%HQL%Ro0%oNWeft#g~-gsiF{Fbk(8uVc;o##vh1%?RF5<}YD; zXf+bs`10BCzGo&YGz`&yz{Q#{EnedbX@_wG_hVxPNXyFngvS))bK2pHVOOAA>S_08 z%XU&62~JzVv%(%^OdG=Ou|c#ynAaV|l*u>8hS6npyS_kR;*WiKXgH`-%J%mW_Gkn*$Qadi=jg%~aS&OAbmX!o`Y^>P@0DCZ z@t%_e$?Zr=R^ky`+`j43dCM9z4;tu(@&bl(C}e79zf(3esljmi>i90nwkpF%y{Uh7 z9He(J^3gtm%)Q@CXUjD6!iFeY%e6v`x==UOd7F|D){xdOg;ps1kAnS_n6sZ zK!xDcPoQ-)MzpMTFH;h;emf3JgkI?Pn~#=QbQ5d}CQd)x++}z(#UCs3untWWFVS%a z0pf7W48(XBwZa-GD@{pP1`OhtLZCo3k&nhQqS=cpm#AY1??2&7{2^f6bDoBZYirIl zO3p$?M&<)74lW}lo!g75e0wlPww*!1-&T3OmYP}4R!XX+1n@95mvbv}RNfXnFC{Y* z>~Wt_ryatZoTcyjM|ha7`kLS~{6S3j+rE%i_Ce*RI1!6VJ|V%v== zl_NfYXy1b+lDpgD;b!j<8kRA2k}WVRuI2cVyk!QeEIoVT%}2D1b?Y$Vd;>DT7dQLK ze2-NO!i1a@Q+54T-*Pgio^0z3 zat^82#3JT%CFL<#4)xHHMfFc)eRT?Xn^{PC(t)}Q{gG&WaE{E=OJM06J)e$X1B zq!r!n}6k9uyxXeRlC!6!FHa=8|Gc^6K3^x}}6tOM6O-tcepN@yq;~*pd#7m2C3k`Idw9R;l<@))*n1iY&1APUL0zsAff)LbyKJ zUIEL_6^~KbHB2>-*|wfE$r%GyZ$yrP#7p!l;U6)iE^vB)ofTzi?2y}ylS=H~H&uTY za3_;=!;-s!KW&3spUM#1jsG`(@Y!x`KMuGTY-Jm-bF5^O7XvkxcXBOZmNsH(+RCR) zrJx4YmX|CQu=^LypK=4IlD|2nS=Uxtx4x)PC;?}b=%72Wl&!s1CO1}-UIWX*1@O$| zXb9uQ@RY8?^i!Dj;}+({Us|FkE2AfSlH^iVQ5iae8(#%4=+$f!i)@xyiq~A#Z^3geRgND zOIImrEfLz*_CDtMR`((lctB#8?RF+ zwKD3SR@DA=?n#QRr?QyhSn|bt9*v0B(&t+O_}#?&6o@JxF=`d#Z={_Vi`+uzq7J)z z%7N?xIX?~d^R(Be-|{ETrXNsw{VcdDwmI${vl|uiY}@x(ZiU$t-j(XkSb3uwDkWlp z;WxEd1w{_Zf5_*wa2ViX-|0?zaq73sAxTDYmp9>~&KbwxTlzHrM3qDSy9o+o41#mz>4sgZc|y$W-FkQyUyF;69d zM3CfaLK!BA*QZ{A@u>;lGp5HgF!>|OtYt@*fgbnQuYz|mnq%cOiRhUbt{~iw?O)eq z?ciRyvJGd>L8Twf`g8UdY6ZF%?=@;*nu3N-n$C~kN$r6^Yf9vOJ6iHSn6lo7YvF-_ zhgW}pK`=1NPv#7-5=nketSW(0mY}iI(qLGvY<{P7m8`pZ>3R#Qn#bXLIj}9ndn1y~ zc7ON-XRE@4tRScB_U4X||7gPRgZhTcen`0o7Ka727bO9lV10)_vH4*_9d)9&MtS&k zG!*?_VwYqb62*XvuaHQhKoWj82~rEJqroWRe|0DuN{W=R&aUsdyoAvlGqVaZY6lR^ zQzF{0zg;ysc7 zKZ+~Ygb00i5l!J(GbqDRlPXJz)N1MMf9Y;ts->t-*ju{jo`R)h87e>?cfn}X!F@y&j z4=$Lz`?MfGNyY*qVF-^)ag!W;$0 zh6m??rCX`o&O-1!#gD&z?i*xG>7B-U=5(7dipn$#o(%!X?1Y*z4Z$G0^zEYWSgGh! zxW!2&ZYGC$YFGf(z5~Fr+jVQ{e)_Q#tIzKJVcBtzjZtjM{F0kN?P1#xgio)deXH%K zNL!XNkYV|7ED3z$B*w9x{ZsdD8Wf?_@g}Vo!w*UAoKOQV_M9NOr(bo_oIWGAFpH@k z-S3^?CHR}$v)IIU_(dNP6=)(VRPV4bVOuEr=(g+-g;gp{7(QbAu7=X4WSBnqZ6mr= zAc*$bb0RG7JC&pap`;YBR%4x(BvO(yG0i|WAd<%W?c~PCXAx_RNVg`PTweWk!H+Lt zen-;{>cc)|`;Xk6s>Pcxoo3fV`u3O05@Wr$=e_S*#qMW6{y5UCYw3=}T4FIONarod zdUPMPu+^I3%f0`}t>HLI=M+4rU)ZAoOE%N8(oFIz?YE>rc~LP$jUhMVk&5LNuajQ0 zQp8UhjQ)2v_~sd7bDraldU`4vX#)FjxId?9TwVqpQn61UzrdS0joBEStq92Q*6iD8 zh$keFJ0yEV2VE_9>O1JDfL4~XV>p3qqq>tW{$`g~xaXauT$gVeOlKs91lZf9q8C(Z z7**yKSQY#M`aNjFy+82k7Sg7PJx@}MfZF%J|ky&gDpzZ-UJoZRWTrhBediQ_gLEnadVTY!0wjE`TG{?me^!Eiv!N zbGge&x=Y~is3qBC;}$5xj!}8AB*-@ zXfbrpgMLLHSE6mA`D#9&psGmpMGo8zFUDL0ruf~FdY;pr=%Fk_`799*h$7q|r8os^# z{O%Q#Rw~*N2HOAqrar#M5U)P)tIU)IgiMy{_lp?mMrD={CiJ2|Gn!2~jBjZa%6F0t zYM$^+2hFO5S*_zX&|Zn86iF_a)jT&tH6s0$_N~O-DiQ5f zlQxFD!QpM~WQUO|&r&8hjBI9+rCDj)PrYxSr8gydxFuwcbAI2OSnYmllPKP5O|S!>n$bN_?6uCg12ka% zQ<<*%`D_j8p*5IR0e(*#b>Ir&&F%eM2e1xC+$Mst@cj6eCa?BGaZ5}SOOKcrN5Zj5 z@?FW`Z}SLKI>DhV!1R&IrYkM1rSX6YWzK~C5)Av0dZD^IzGE8sXnrYz=aiO))ws0W zt7?$p^Em!_xSjWE;i#}`~$CsXnbw0(DRY+kA z_Q2EGexspbE0w&CiH3g{C1gYjYGxkA)P1FVe96WT(>+ z{nWY4i(61>LK@)t9TVsqGRZy?A!Hur>i0!Cq0auVPjk0FJ~J{h_l}#_bWG(ij)~>} z(qlip(=RYWYKtim?JHKvWrok;jLaEtRsjJ5NoK-8oIS2Cii3iSM}E~E!xv}Q+7)R_ ztzZ3$euXR#4SsFB+Ym|Ctf4&3xDmlsKQhr1vDy(*zmhJj)!NKRy04!}T)>uNt=0t+ z57msu!<=hIYqB((Bz^ipd_r-s79=GLT&!*9 zW2nT-ysEMZ4NBvSK9~9H$m;)ZM@d9AO2G+gMw0E zmR;*K$ULiSH82pYm}|9@3|C0XI_llt!v6H9{+Of1;|e)+ef~L~o~kd}MYKjT4P1>B z;82e4w;PirO9vWtJH5k0`#5@ri(k*smu}*=iKhA%4@PjN#usB#usa=)3xFTaNAxu9 zQgBDkHE}uZf>bP?e_y-;=X6y0JdEyXG`2FzLILXJzu70H3cu+3{jN%MbaFSf)hz2& z$c}sBaxa-F`+ZH&xXNOPKgQH}t-+Q5v3%UHiLtyyB-cUjRRg=P>D-)z_Kz z?#2xI$k_8}h?E`;AIz9sV@oP7IlD8E4Uir+ry;LxNzuBWQSL&m?;NbzF3@-F@=C8w z^cqE^7aGMR^T+LXOUH}G)x@Wa!~`PmGIOJ!HKHBtP-t$jIlWs-_L2Qwq@i1c0jL?# z(E@Yu2jL;GlPVP1?oXJuEXUNlTH>5j`$#?=^YljK)ro$1?eT}*U&iV4=zU%c6AOey zvOZ|dm;Cw(=-0>LUY~MywZ$KY zn5#i=8tZEbwwOhqb1d52T}dARyhlayt?qr@ujDu(P3U=RDDlz_cJ6IpzVO^xr07-Z zMY17a%utNPyFs+P!twOzGDx!Dx~i+(>JZyIhvdcC0>d^lxls;ya_NU5X)8_ra(}X# zgL+r%7s>%#NnHO96KWi!{IcDymhT&h=Z@7oPIHtO6`Dr13P09(85>8m^eSyt@;>BU z9&9ZRzx<1UeMZ0^2bl{F2_3T?e0MZ)uXq1}=B|=^r@C<6scSY@ur!*rV7gDfgve91 z=;-&2s__}>$5_rX|B~aG0qD5{I-~8b|EvZztSV~|+jkQ{sv2IqFz|OMj~Z6-Kk1}8 z0>=5TE)G4|gM((^CU;NmN0JOMzR^9sbhqlvscG?31HVn?{&tP|K`&8b>qkxS03z>WqgrD+TZpe{^7hu6s=Fa_T+twL z{?BPkYuWT^X>ZcLh|xTvt&d5zTi}IEqvq{7=at%2OQ^R-KJ*;pNObJwQy>onVeY$- z%j+?ovFP)@9{v^biE%Jcf+i68D}tdg`n=k?v&yR zua8*nY&igBj0fq1cd9i8k&(3!EKykO$GkQZKwteiN6~f3!oqXgfhkOWmYh)oJP(pOg|GKLK^_)V**8Q z1Hx9v0l6u)VS6{SnGrYn;_jUDcH+@(v1<56*saKExGSg*7!D}zb7IJnEpDH#skq)3 zQU{+cUq+e;IPWzZk^Gk}h8KB~B7UrtGd1v;arfmg)&Krv_xyPWRdJQto=8P4r$lK6 zX?`e2XZ&pZgR)j&E6z18fm3pmMG(1{W~Gq0cf{c!AI28zPp{Ej$4nd%3`;(9qJ@t2-op01b}p$HO0-i@K-0xpwq2So4iH+vqNW5BIt zy~W*cqwjb#!)`{#CzW@Uh1&)Jtfx@#z2}a(2mw9{^!I^wQONXNZtxk zlqeEB(SNd(r55xC5grh4)DZr|UkfH&$_e_g6LEM&plr~06)WQFy6c=r#q5nslt}J; zO)Te)LG;6e9LD0j%37_}`97`Cyo=+x_k+>Ctq|&nvQ4|MnLL#h#H~L$`g79~{wHsJ6&@hTcAW*fa?1RQ zQ<@t6wZ!PK9SJi%mptdFBOast-uwXUSQr(g*AYh>UiHg&$MD?7QN;Y~O zd(hf5a-=s`^d0vh|CxuZH`yU7_#>?QBkr{gFT*ej;IdqYx>1IDb!IG#nrbIK%e0(E z`7`_Nr|kR3Z}K#7#i2*~V-#slf%&b=5wjU5%kE>2Sb6_%og$u55YbVKsh#@QvW9s_ zEq#HlPlOlg<5=($5RHzU5uiFPVeGiAek)oo3JfsBkqAA?Mcl{@T+5}|$TeB-?x{MZ zaNG+z_$Ck*`h`|JO!=uHkL;L`q-vu2GxU0f@Y@(4)_7O7+IvnBD7v4@wBmTO=iK|H zFP>V8#IV6}xm;^T8{SD(s?+AN;ra>ced`F^*+Z+JWQ=6ewHqJSuf&(35;T|Pi~B#DojGr$A~=B=_iQ@Du;eY_w#OZ0WDq@O$xo}hYPg02AUeVbe2BwKWrZ}{?x z1tZAdO&sQvLf&ZHT0Rw5G3}!)BL$w&B9RcqTYs8Kr!VU*kBs&w(jG)@=4!$x?zH#Y znwt>9TM7>?CmfMai>yWM^ZSO36bSp=^KEbk6rSe`Kp=xXcc*@WC;QfRQ1p9D=?U({ zjPSWNjAMiSvPvWe^eB<}(j>=Yl*jf>HDzBU8JlIu zqpC2&F3ztVGDqti8!qz<+HQq~xn^8+JehF6k10A@Nf22Rl;S9g*&F?T< zKITR^or_>DHtMRxT)}dOkjrDib^DlR?MduG(Bee@FPVQ8w#i7s7Q~8-)V{>i6O#iS zXMgcs`4&^^Nk&vF8EOaYdy5!@QiXrEBkCc{B2x9mbW^k{F#1c_!TTkpI}6d@SqKd# zv4MF%aX`zsL0QcEv)e5r7?(&99zKlCag@A-!rdw+yi@T3_z2PW>M`kd&4STq{BM9{ zLiAhB=VdeHc)Y={wG^3N|3V)1&m(GQ_%N`j0#zu>p2flFDD}z!mCqaZfKv z8H80JiS}4p0XGigzABvPAfw&6aUy95$<^mw@7}h%N&y&2oIepP-582_&op0>Esxx5 zCfwERQbyhR3SL=X=L_tT?69k@@A5dMnp6R|q+(j~t#2sT;|_aNcMnkDkUuDB)e!9) zHN6ewqdMKlar)DKZ^9mu*OAB5y^2~0XUY$_7$MK-b8GZ)tn*LpQWO`U#PuP#P*K_7 zPpCARq}nSUsFBte!^ON*9q_0W77xeioQDRr$5>i7b6*gH`Zjf!~@It zH69Sa!|Z!_1m;;cxb^6ILfOeRiC?IJ_sjnJD(Wvtar=bG8j{6W#*v5TJIO>i{!L{q zuS&fAV7CpY^HB@yV|n(m>yE%ckQzzsklKG>_(=ijsN-vl!|&lAMl6zY zqQApCe86ouZgbh0yNf-QhBjg6*aY_ef`20at_?&6O>$PnBq&6n2Bd z2QFufOxSOAI#sGOiF-gXZr1{D(%^>e#0szL#o1UhSCtI>9|6pS{@WCnV$g; zasWYotkc}Xql9`Mq7Ozn7CDKzM;%|#^nPn0XNYz25f8qB)EV@QZn<7(hWeu2*{zRy z%(-K;kv!Y&aAf^gNx@n&X7lwen+y-&4LJtNb-Q`6B@j^{Trzyp*eh zH2MO`TIkiDW4s=3T~~X|g~qr-9@=HTZph82-nN(q*IimjP|s3($iqr*WxJFyt~ejG z+Y#G`0hT-77w&P0k_;xDeEa5+Y5qmdD8~oPt0v{)dgXMJx?o&9cz)i<`C z7yd~?rMmdAG8j}cTq&BrAn}dSLON3Ax50u*uTG4)u3G9x7ZZi+Fp7|5trRYa@*6(dSpp}RbFbLBT z|4}Q%3yJeTejhv(e%%SpALb!kY_VifoJYINsmzCNSY3_5tErb7vNKyIyVdafTCYRTPujkOxYD_acGix|Dr?~ke`mU= zq+O-JWX*wFun`0erQRNAWtUDn;TD%`^%cLiIB+rBYT|iz((!2Ve>y#2lqmX-Thl9v zuPTE(UZA$6qQX_z>!`Kz(~kCMN*CVvZujLCFE)rP zb@PLwL5n(E_{b>%J}MMl7&p7{9fCg~@~$6Guf!aDYeP$|Gq^j}(8`*E>QRlo{ja{F zd0xzBQpD%OHV?}4?BwHpZeD_8*z?t;4%OdTroWp|{@IZ7zt8_Gf&Z1j|GyIG0w}FL zBTAss!Yw>%Hv;@mMnX}%Qq(xe#_T;G0-{i1gtDR(HWnEc{3mP~X>nBq1SI$)5(3`; zVehTK;`o;JVIWu%+=9DnfZ&4#cZc8!?gR<$?(Xgy+=3I_C%C%|?k+R$-Fi{TB`<@6CVC|94E)~={ND`x z|33pS2(XtQa<`~3U|=z%HQ*2z%s3e~{8mRVgTN7#K^>YRx1us53#nF2x(qJ|rwKROf z3yrd2%EP!Xw9D-Z&t;v-A08EV?+`2gfa+&Q(L5c%+4mmN$}_q}xbLdE_7bd$hRHQN zI<{rP!M*b%KhcZBKM# zcJBQ!Xc8Ug(lIHyl6*eEtd0%MMbLf0hMV-c!76q$_Z^kGTm_yV9n+_za$YC2 zlDc^P*6L`pqqHD0-8!~Q1J~Di<)&(=x-<2C1-o28GGi)J?Opl(625`=|*`FtiSW&0l0uvc&C=n*B&kL{`-GAL2{ zVnsyZn!#<&zWnVSORDe?dc(b-)m|;hUm*Z^!%eQt%?PtiBEyl#g&+r=$}!f2m6-EiL8%BSGJ(Y{hwV};Clp*EzS!8A_hE_Mm zes39l{@Lfd4F&(BLZR(8Yt`K;G}YfQT+V0akWx6n;F1v-n-^f`)@1^43;mKw7&hvz zStRWP@LXvi5dMswH1l)jkZ>S;HK&u|gZnVO6KV?HR6j>V3+D{#OzI1-dHe@v!3X~Z z>Kq>PZ6r19WDX5-Ht1cyDRK8a10+3;BbhCS=x zY9fm0#lUls4P6Y9V?h>_b8lX`oQ$~~~BS;6YwHpG2!p zotoMGZk1hBf}zaPw5*WS#Qo0C4DbvP73QSn>xF@(u4h%u7Av9MzWSfV?GEthR-k3= za!hnK+3f4hiR#E@+22LFDV`ytHQ@-=*<@=ymlqA+BD=*zylcW7>n~$awPR9L36^D- zSwbj}^2e1W-G3P!uTBD$ixW*X@}Hj5Y;!%A9@#77Vl~HAy@hE=_N-IrQw8NsOw$+G zM<0Bo<9DXe`gHkfcCxM*yOmrTaazBSZr_z>P&++mo%%N3$#BP)*(>@GSiBhQHtF+p zwX}J~FH~$FghM8$g2fO)eo=nv}bX6$5V8rGucR_z62p336>3XzB;<>pKJgfxq!t|<8%tT5iBjvw;g zK2O6{-D204d3WiiQ9rkr)^A$NvufSW6NpTuT8HR@&PG_ADT=}tD>^A$cBPAemVCJH zAJfRp&87l3?X`nn{e4hIWDw|?5X{d+YSeU(uo`n-_t;dG;{r7-vAE!xuXhxJI<|Bp za7(56jUWBUf<9e+dcF!wrjF#}5$-W7fpH?oEf9&1Oq~mC?<<=1z5AZIj6dC*qp8uc z*j+_hlzdf;qoA?ctv8AE71XxTrIicuA-V^h-A;Cb+iRY(G!)B86V_J1E}7leIY5e8 z_gM$-^c~ z#s(P>a~x~Uy4uYJsmC3>-`bcT@jktD0H5A()p-f~_U$@2JWWtQ?yIc2jfNqR3b2jz zr=!ZJ`H34};J)6@8Rxx=-C@NL1o~=vmF|^07&~{98%W*2_r<$fAO`>0%EOQwuxJ|5 z37eg#>pbBTEZm|J@Jc%rcL~00AkO_tiM|5hUfiW+DaRs8nZUtUi$Fy}Kd>|LeS*HL z3{nZ?CETTCiTu(t#e~)>d}2+gXc%U9zh0c}1tNCqZeL~WPNq8>PgN_@U$$Fm9M?%W zc>N0CN_`wodhnJlvPG{_cu+sVeWjN;PaYWCX{~#cg@5r5sz!z0{_>rkHb4Huh#?Jv zFQDZui84A5BW9qoC^;liGd=Zom~)NjQsFK#_RrtWJCIa+&y49V;p31eqD3g0J9PqQ z-w?}BhCA6E`DSZK`EGTA9!c0ueVjc3?g4W<1Jd%-*_@*C*7PnKK4IWmqc3sG5{&~O z6;3Qt;lr0y@DrSzt0|)~8#rgbQTfA4_x9X}SM_+-bLU%Fv8QL#dS{i#`MTuDMzvOM z{{MJHB33kcXSx+-o(t!Z&z$!jrXQ6d-E3#4N$QX@?e{YAw|rP* zit8gRZe@cCZOKXL_aN!eAl(AOJ(})XHAi9F{92-!azTYsDlMvVY_5XMKZd-`JJe(O z9#oy-jRuS&*@vEwgV_%1N-?-(gS0f!ij0gEbZi`+nIA@{Zfxg2{H%%A=xbCKfq|b! zfxQRHWR!nod*0SL6i3R8rOhr4*j{pTI?{7eKzsNfl!~%S8U4>s!8-3BCwnUjsW6?7(w|5hWV; zn^QXB9~~uRiK*vV3XVlL4k%I2QwbJe6InlmuK>j>Ep^269lQO3*Cn`?E`w(P?cacS-ujT1qm=M(sdW}&G z^ZGg19N}o+517UloY;(@-%b7GEL6kW{B1ZPOhFyo zI$UJGo*iglrcF4faEqQCTKH3XbG`B{S@&3ZJn<{Z>kUDQX3lims2-K>+tWBr@fRJi zSe@cHUY~WR7`ak{fCR9fC7{~bhjA$c1@1Vc>=2r)yuW?KN_@7rCf$I z4PvnWdNvq;Iwg?_#VRDb*GWZN~$b z(&BtJa}%E9`rbB9S}C%5HafZCIY&}2L&T?}-|Vo8v7M^t*+SlB_ewt(zJL9nRf6#! zk`=j_iTZ}W2_kH1&QD1+DS^ zCJEZm7H2vjYHP7>1kk-P`SZjYGZ?eyF-g+(P2jxa_3Q4Mk5KTdP?O8FD=3|~5z_E# zS-MolfLw$K_YtNj#6W~ZRnR{Y`<$3`Jl{g=TyK=%HF{Xv%zVKAceh!kiTjf3z>P{@ z$#yj_HC~HH&yus0%JC?Jx_^Yq@9|tm`K)*A-PUe5*-KaX2c=Kzk_i8ieSX;DWcofo zOunA;ImgDR(z2pJfc`ehvdg@@_a4S_y^zc~+w+*p26{u2lH~!f?-7X_<|&N|>)ssm zl;&{|g;M7}F9mWD!{7TXKl^N&i|0?2UEtcmnGCIiunnurl4$>@aU7}Bke=As%BF&m zw()>*TcUn(`Gv|O5m9wZwUS=sA+7}^LiIK`|DgTzgGMlB2c)qL74+}jEv*E<+KTWg ze;VV88F5;X)RCEFOWNPMC2}eiP;@n467odhU3#O3iAk1$Z!^~FD^+dN)#B8mYqF{_!^ z;m2L?`g^*Z(Jy6xbTIs2VHQL}$iJH^<9B9<9?U-V759pjIqhBOz%H$iaE_=>@05+K z*2TN&%VS)x4@+eaz%Z(PnQ$C?>Uvn0o;C+!g#D5Jm!w0P0W$d6IN0L70|50*h3w~6|ayl8AV-{}^eI+*{1ZMAxa~B!2k~=!+E^T`x87*_)oebfF zZ=6~3SpIWc7oKp6Yf?Mou(V+M@PtQ-k8CUn_J3MRWk4erunIP3~hI*-x48XRLwix0H;yJpTq@{1h=-RQQ`*oLO!TpV*ZC(GJ%J)N`cjo z?@eT8-r2LA#y*~UR!>Q(GPGs0BLY2aCLWyXx6HSy?|e_OVlg<#zK=o~Zv71bjVO$) zET+=`rlAlaSAgy)as_8>#zz6T_4{WUR(zjeth3WDPI8~5R*UAvEGaLc>W1Y{V=wV{ z+tLjW8D`T=bKS+{TXLG$()E{(?326R-2ymx|5j&zWzp}g#h^)``1)v-__IZho*3ve z5wFMfPq^?I(11dCxZF{Lg~mFPs9=3eG+41USMuajJqo=S18WFlGY-OEA;yPMwOzZZsGXpAZ#20NW4?kt}iB!lowS+?)141Ixyo>ADj5kGp@&$UJwDpZu4mH6Cq`;;Hp z1#^i=sQ)uZemxr$Uor7wNcXf6c5x%*nnx3^QoNp@W$wnCrC--B=T8xQEPWoEc2ufj zI&3`M2~o8W{aHcLzY&g@Kf=F|%rD-R|J~x{`GetLSXiolh#Z@v*PQF^1TNjdTF8JQ zi`wC_W7mEv3++t7J<+8)n^qY}u3=c|A+g&Ojc;)#Hh0C>kFfopp@A0)SX_d`q&|vWi zDs^?J?X}-ye?3A+_?MCVZ{SG`TP*1Q-NY|KX^jpTCv`*1r-_yvXr>Upf~`An*PG;e zwaD42doxxWGK%r1kmIqP5&JyhyGE;s2!IG`Z~#PSyvGo_O|(mNl+vWU{C0W(ji;mF z(0)%x4(;)C3T~ieyzje`<7Ws)9sIu}9xD15IB(N}t_ipe zXycZIlB{IgPingquUdC4hDr;&$L#J#u188{D)qP=LCD|b@ZoZdw6@eCG%#;^e6=^% zX6W)-6}9EvUwoIO&F!?pGuyUAeaMcf*)sR`pRXl^rVRQUTV>KE>_!hSxtEGjBOo*9 z^jVnyn21d!59h=wz{rRwSxLTA35L zyPt3gdMpv0e#N*vE5$_2L9GSyUu#jjorg}b33}2~_*Ed=n|=NEHRRl9Te$VM$k zNf^^L80Y$kdGAsC$7HWZK;qM(q^dCJ&JE$;7!rEouP*5~=TWThevuo2iqEi7BDnCx zi^f-Sd>#uvJlY&%pJM8+R{JKxZu?no1z?Ieyb2TIr#>Hk*u@Vu|Ie(78mQy@6%$Um zEa~-KiVB1a`BLW+xLswLb1z*3W`r;45>dqj>cuv8#(?fNe)eWzjUVKUZ~nOwVenqV ziHmDAu`{e~_=lCLj>OZ#k7ZsaB^S3c?&du7j(pFDAWpg{%@9A<7KZlYGFQ^egGCg0 zFeX%N!I<19H_$jfE+meRjMaXfS(NGWsAqrgeX8~P#8t{beso4-T1kY&Y;})OeI3v+ zIx1ir!h>n-^dFN1odTJj{ty^k%LP()2A+j&kBh#mQ`=E(1QywB1H=a7H_>FFTu+hSv;LNs5AE*Ol>Bfi3A5 zGhNNhWyK>Yr-21^fE11Fc9BnW;~(nTf9?esc&MDeA9_O%&3NILtCJb=DD#n*oM^*z zWpq(=>UV^YM&l9$uPbfDn;u(=R9&l z_6cpp#;Yok<~TrvGx^Y$SXvA2W`;JVJ-4wLv9Gv`rQ5A-gnwp2V0c~r4Y&SM_Qvqz zz4DB>cM9&gMrz#00?zF>nYyfZ(Z8%2M2#mw_iukrL*z|RO_Aw&kwW}F&9|3#we71L zim88O!E{^G(B3$F2JsWNaBx){6z&&g`IA_uZE%=lN7LdK+7*PJq|>Aeg1M=NDPK6Un_qTa|8t!Udf)WCj%Dxud-iq) z$BF_`Z6Izci8=9Ov>|^t;UB<1mtTr5j}mV7kDuHX{90r3tnVz*2$A7Qm=2*2qr=1V zZFUd6Q93Rw z4g1!a`stlCOUR9#QSqWUo2j97)TQgoEB06Pe_R?CV;BP-5ajgKQ+S0TK^^PVFJO0U zFOSxR>+Iw>o*vuf1s)2}1vy8?ey=M#N-#7>=C;^1rh=*=W3lvnw4}MYWuV?wVdm>j z1-g#5Z^MtoxuIz@MLvE?pZvQ=DtZIj_Q3~pdJzlee;6njkzz9i(^=jjd?=tvsV9b-K7Hkw4f*K%1A#yDO0sy5^ zTX}fz1`68$DBC?;+n~!deOaCE)ncOMP7>ERm;vh4XQSW(rxX==*xaH(UGPu3a=-PrmCJ-C$Ya`9J+o@pIB3t}6L5jFzca--?-tB*& z6bxK!upiIk74*t`CVUyL*0$6BMICoXf*hc$dwq_*_UIpapvCiqvsJzP*u7O=={D#T zi?kh24=t0EElg>)?hYr`@!;KnX`IsIKL6(FtWo+Z0Xqk& zyMskT{D*3TffFCa=ox|AlYERP_VKc_QI>CpcE_vUufcY9&?zk3(U*a1`K8>Nh!FCI z1oO<%yB<8h;5m2X5cOdop;2<=kH{QemStx9)IK zwq4$2SZkq3^ByigC&~F-&`I?jp-93Y`k~BkpUZr}>1XB->H|)avHUu}!ULIrbYw zEG4{{h58^Yo18tlKYCMTi@}LA&@ra31)tF6jitfIy&*Tgdp_3{>~H)X zp?9;9j3d)f_bWbYi8Rb~vMZo}8@waJwXd*j0StVo6 z3*boGOxw1?p-|J(F39D5x6l3Mp-c>V&1t3uWKmm5dqrd88|&M6Q7BA_6Xkz$ofrq{ z;VopV#t6Cq8a?kL+*8bi6Zfnp z-(pK|59@23g_Dmay*At1c1y={iLEbdTt8IvH0LfG8XKe&%2{nVF}(De!o)pBd52ZE zQH%4__#yi_FW31_|Ky&$r5s@}ZHd(HUA|>evlkJG@gvA_O-;e#H^kC!s3}HzNxh_sL2qp%YTjJ~-0TG}^2Fj?jcKp|86ZkpXtS~zgb`lA zZqY3cxDhZoh*C4~Uv$V{=pmv*w>dTfEBFg@z(g^FK!gA3>ug?;l0Y&&7S^=ACXg&Z zf1+|b=_<-6sSfE*LNFt=fZXsmb_lT}<@1=({M~btxNX}n ztrAL!F(lU5ZjuqOj}&I)|B@ak$Iw55U&x>(p=pKb)>{(`(TQ#EG|)YS^hKFD-AS~> zSWes9#)j!l?!R-j^XsVIHl4}Hjf1^&z;`hMNLHu0 zZY>t={0dC-OWlkw)nkJ6Py<+OjmPdt1V2p_jWR-^P8Pj=UNX;nV!Cc?4UA%bB0mLu3hzU+YyrD#42< z1vkKCq7YcvqzBg%uDsPkQT`-BAQnJL-;0lhGk6_v(BTaaL^CpWk6|Ue`UxCMq# z>tj(yLtS?A=pT!@>I+=lQe{$}tVSY%3ejWxnWyYlT_D*dlchyzU_eg0d}RoA9cUv| z5aM(3A|SJc3$Ka*i@o9VxbUTeny|vsqfGb~TG?b(-NQ zJ>8FD`Hrqk>-OeZdD~L0h0JNR<^#j;2aX&Yi*%!|mLy~6yJ-Bwxiv{c2c&Gi#lDiE zkGw61872sZCb$FQGAUK!?-|Fb0{NkD=;O1wVw@|Bq2cNoKPfwJ-bOaQoz1=%kS}## z$0%@b3?sd`+#h}%{8Z3qNgaIc$hH)Fb`8pP^e%LBuhuJTE@ZQ4-aVAubJUTk>mX*^ zK~)R*PVmtU_utmJXNH2@F+z_Kt4CQY*^?7H^q|Pui ztNXM@C0UN*>GzZ&1t=C6Qn*4JTGSs z?Nc$bh~V*YEW;tv1ij!FfkQk$5ggVPwfrAp(2kGQd6C_UOK2x^9h`l)`8hLB1Hd*{ za_yJy{ESw=rN8`yJhlaMJYVDzD`{J<77F%0117K1U~r}LXr`FdyX zd#wE-TrR{viSe_jnGs{^my`tDL&C9`dZx8K;PW3CQ|UM@lQK;Iyb{jO|Mv6T0Pr0_ zEtr@OF~rhHSf}XQg-W!!XM9D&TEB_RhFF~rKODUk#eFp{EvjG*mxlQe)<_9eKum(S zL$@!TAP6VOY=1z@T1x-$m{Zkl;)cNTjKhqRi6nS9Q^QvPQXXf31GQhiiaoebtnXAs zsTzHfzep~9i5Kn<=izg$9%lAR0%cVl(iT_HR>^iywbU0181`cD`Q5HvqZ>GTeRkdn z)g3zh*<)zvwDZebXsYRIpYo9)_2X%(1z ze;TAR@3Hq@_!lb8stt2)TZK4UDBeAOG-*qp$mlxu&akp6yFj}(m|#aPLd9I0+X?B? zAkD+J!5C(EKdn)1@4b{}t>{QJBv>|tfsc$2_b>5!5rm@@7XDEC=J#F1gqrZL%c?mb zORmG&I+6D};o56YiecfRcumCWUcw3C$5C`B6qEc5#U%NUlqwGIUb^>R^cHzYH(I-s zxggab!wL*y4&&%thF(2wXOJc|hZVpMR${KoB3Bx)M?H zQzsF?qC_A2!^_0WsS{mgHgqc3S<|A1rB!HTKK z#Rul;gg1%dzBO0X!_VQStSVcF!DhM@7`X44zw}Q>r8eNa^c3O|VTG6q+H6b1ZG?A8 z^s8`-+Pq2ssmPF^_`C@dS1R zi9;d>v^Tb0w7ru(cHDf&ui`i78>N|s3cq%B!--DKIMwd1!d_!rn-p*5Get%Zu zfEM&KUwgFdPqdL#M9lr-XLSneN68!;-m;I?dSbwQ@m4{nJ&p$0!F#@q>}6ob)?4@G z3pX!D^J$7Mc@M{{&!%Cum$@cy`(VUG{@auce}tdC@w54|5P8guQ*P7};u?MA}0@~EApQgYX-P{V>r|2YrC%r34#TZ}EoudGHtxQGM zYN*seA|CxGZi2XBw$DjhelAmNgK3a=fNA|TM0S?-na-}&h{rc~$4(Z)OlPmrCjP2A z?!BLL_WVGCyuD&?y10BPcpa&(6UK|>H?VQpRGC9fu~l$$WlyVCHO!bA958ZK-o*5taMgv?ik zJI(n5AbYaoo~$(2vOqqfv$OgZ!cP?am6@@*?iYT*Vd})Fx@t5Z4N_(t3~3-dafol4 zIJV}B8*>LfrH7ik-FK3Tn$J(hj05BI$w2~eLU_MHDvTo*Y^B3c`vMmbmq$F``OkMhQN z563+Y^^NNU$lXX{REK5ED^XEhD1Q}Rsf%fTQso-K)RrZ;a8+6<1|E4mwW&N@kfKDO zBoH7$b*OQJXq6$sCyl0o`Sx!z&EFA6`ZjDEK%L-(zT{IsH?4Gj^zs@uH^$a6GuVEf zGAX0$xSn}p(=fiD#HdiGA_;ZH8KL1C5ysOr;qrMskViDxH;pMFZNaZ<77b62H&)nI z`lGEbr?gIHP9JQIMkKEoW2;RikPzUfX<$=B4Lj&084wIt@?U$xFNVRDR8Zhjni*^_ zzWBN%O4c}((x0<_`c2>iO}(V6)m?fdg9|ruW9(9C<++pu>0u2DB9{aWkk8$NCxSZKMeL_|;l*XO?124NNL(J|>{>JdD z1^-|Fs|Xt|T|AGlm;U@fYd0*-(luqnhh}oxN3t~@0gd(QRb}n+*8!#?!{cOG#I8W4 z%3@GV0wFZ@Adb*;&-XZKs8x2Nrz&E8gh8SjyGH-st-(CAXtOr1ZCwp?SAc3%d0?ZE z$4el`Fj)mn47AMg^IYz<+~(X1Unpo&w(2pyv!->8qX59afK3gFJS})KeX6q-69ixS zn)hTEf$`Q`3--%=yDRGp+%nC3uXh%i@Jue-^}K~4@U`{1bI1;X1-bid@xF5~_sRh{ z;5m~4{k#2q_?N|kZkD04ESTj~Vmjq;wht`Fx+R(7b66E9bx&BG#H$DOu4DxqhDnTv zmvHfhcHIignCs_EuSCfvaXY9SD)b8|i2lBFf{Xu6lWr1VDkl;!*-rKIBX5vlUkx=i zoo48?=~cqju-Sav{&KT=@F7uaaa6z>^j!jGxJIvO?IFr4dPXP+8cRoqO_CKN-TN7e zznN4*5xv~$SRkTi&v$$?O@n%@Xn6c%=~xqqaSJoCEV?TeX|~v zgL5puEo)Q^8DBQgb&fjC&0G@D-6Tnf3*W3ZGW(0QyJp_~lr{o_1vj3Bd3ztPAd;-5 zfj=h1Sx!OSi_zGBGDj- z#l&azGsUGXQb5w?ToJACvzLPUT!SnbP$`_l8&_F+2N~Nt0$d!g_fdR*-#Iu-bIHPkn?09IOH!|BC7H{9 zQhFUv7k^y7O%88XSY-cL$E^~?U=(M}eeNo8!j*8%g~Jua{tS!+@p+?yN*(qQr8$X} zr=V0?Rc(7Bn%UL}h1g?!X|B*0(tYhj$H%NcgLR^LgV#fXEtp)uY3FtzY{5}Ug*SkY zk|wXjw`>rRv9o+V#Z8+#iS7CY(F?W@Bd$$=wCT-Xf(}KIJQ110Y%Kl>m5=Z2%(h1N zcBjEbM^#Acx8aB2nqyEARkyROceJ(k+Ln6jHjbMD3p7wC%_4X?bn7UQ_E5lOh&H+b)}1};OvyV*cm^ch~j28otH5l zA8VYjSYXA@k$pF_m*pxrNA!iLcsXc3un2Hdcgf?NkqGIt4YRbCn^(@tFRk1e+yUbbG?sTYX z#&ln#&%NTz5n!MxAGokBMrXf$+`)5GvKEgLDqUPz$y(uW`s{F8>sBU5=DVyL&OmNx z#Rd!JVkW(2IVCj{YZgOvs|k;vCZzA6;Ta_wH$JAy655nQ)0p=ylmx&dGNxU}7`-wa zQ9{YW*?S$uL{9pU)X-}VI<_SH(KC)HQ??^>ll)wAIszHt_o~{(X_=qprwaJYTYbNF zi5t7QmzCEXj7IDoZ{r$Ei+iesjyec8;?@u$CQ%j?@pp9qTgA>e<{zIZ+j_>1zeEcT zjosgVf20ZpbJgD$OnF(KAGm2bb`PhTVi#XbE3*eFz*3w2!AUL@WyZP|*EA4Lp5U`; z?LLqLcK~99l*RZh(%e=Sg^>27wfOK?Uk@Sx@m!{YedK?{ef7_1uIvXfVN%1b(5qYf zBcF?HOEj;&@vsG5i*_+$StNyjV2Y*RkJUFV=h%&AhFFSnjx%?_{fj`)A%RuV0o0tG zGa#w*3*epJ8#wr?BsWy*u#jms`M#bKonIveFvb`tmNPB*Grk0wCyA#9xedaP10P;^ zU&*kMU8>Fw6HfIwoWFOw1~Ntsb4)fwnN!TvjuyJ@Ive75m>7wAY_+x+z_L7*?5F#P6q{d zPXo_u$Y984xo76pqIvX+vszJZNaXf8q5k8e^&`K^OWK1CzdhLsZ8g(#8-txzR_^4n z@!(L(OsgPJ#SB*)vp2eV0tU*r0NEv%m+19PJ&SmLnF)9Oeem6YPoFheMce? z^OncEY%B?(6rO&X(TS@M&>X(u&!#)#Lh|wZfQYs$*tW8wPHn+JS|>0ng=9 zBZU%J=iJh|&k@n;i^&8(W$h4#r5^Bez-;~EdzNNE2YXx?7y5;GjgjndJP(#u;+33( zp8Y_H1il~gj%!KzvMSSOVb>wOhDx)VGm}N>>@U?WW66LGfG@hKqPBU-#!j_^PqIjWDo*f1$-p^TjcF zRC}%?3zW}B1PC|Jqw6NnOJNDXm7HOo`A#_2vMSNh zpX2(Juuq5>+UZEf(EiK?Zm8=RZBi3G)^gW|mW-}w7e&)-S75%r%uR6DCgHihT;;>% zR~4X2-?m)OJ$;HHm%_{#LJS1og*%cHz2m0YP<_o_p=`$3(t{{ zbF&)4Odz8Zf1@IXW9%-aSFFceT2CO>G!qL^e_dN3>sCk}sOs4OiQ=O*8%IwGml%7k zWj4}Rh^aFhPFdp#7x0)h#a^+9@_(Np4`ZmH9fmb)2EdB%hadg&*p$=BBy)FCw5*g>?WWK0IqS)9shEzwnY2=2WIp?I3zIYSuUY_S;+lo6*#L!A7_OBN zz@YXP+eOu>K&wj5w_rezqv1sh_soOed%}&otCkT+@S|x#!OlDMW*c(G#mcYmIX%xJ zmA7raJ0aPQ}FLCN>N<6tL-zay#d?MipNU zV)(f``m3H;VzTVzd7~BQvB>^TMdDlV!eSd5#^*}%OWvltCOWq)`a-P|5sKz^<$k21 z-}ki-5pHX&^ptk}6AFTu1&lc^IkcC+&Nm`zyKp8(*TA3q@;<4vbe1tDVq%}S`VF4A zW60e&DJ39&r9WP7>$|JCryf^7SS>$1fY|gF?RQRDNxNP;y;-H=enxt!fG8-#Wx|th z8oa>%+xqd*t#jBKW%O)h-#8;9pjDvFjo%+;~P1kVI6eQ!*xK|1VI zsTC~-xBnpfGKBr3VtoricrsY!CX+IIqVyL*gJtvPv7E;!jOmx{!n7P^rBWfYvj5Is z?ct;&uL#$nH3wR*Al^*2Hs4j4eW)37cGCRwTS>O!z-x#@7B_WQ zfONEh`oe@1E!Q-1`mVK8Zw~a;LgUk2bcNB`f{|2aums%8Ug_@!p)!5?Gz7AF*O-BF8`0R(Xr~@=d;ihXjPzc?(_qC__z-?7U@lT9MpxQd zT>z~3&hr-u3(4A{byq!-b!Qoh*4(J?k=C$xN2%I#Xzl<>y|8@D$HC3quM!YSt;B|| zN@bTCLW8$DZwsnZSI0yQJY=Ez(BpiWj(Ozk=fX6gU&)+9b>}C5c9d#Cg)T@RcTt_8!D}O*7F)66R)yFA5c*m-Z=4Mah!ykIQzhwTKTFG1ndE|SQ_nR%%z z-AKDTAJM$Sj5*W%71Orw9-GQ7Mv*&c`LH-+)$|1&8Js+hM{t!NE_e#O_v|XCllrKh ztEF*&BCl@dT24zkUebr4g?5ZaHIu-YU(MxmLi3z^y8Zb6lJarDl)4X2$nw|~MZBWNfW7cjh!Ul(!K4ub6U=F&)OZgR9p@Qp3$wz1+CKhs^8^JpK092c7FNI|B#% z_n8$M4i_X#aey)v+}2@?BKc=}U;@-z2PS|aIqh{$$pD5>^v(RyFERc=L_Y! z+eYPM=}td+DKxF0`Z%)Qricz577s@xC6S-9lh2!Ec?z@!@JW@%gMHQOQWYg@lxlT^31 zuG*h}uXnnhVrIl(xN&`J>KwA-uLoOxSTCXfUxIgvUHguW3i0McYf5&!%GS`iE*5Rn zOUF$=Ao>GW?x^cm4dV2y>9c8j=PqM{;!L^E0II*4-BcI8j?3Hf8UV%t>1v)sg?0Sb-SCST-;=qM2@|f3e9wgP3<|yA+-e{)ixL<>oK<$n*fgy z$NoR2-Z{GNsM{Kj-G(Q&8r#MRnlv^VtFi4gwyhJpv2FLnY24VhzVvzSz2E!y8RIw3 z*n6)v7v@@%+6lu;aX&@xLhpo6esAGDWY1zeLPfueX}p$@QL?mlBMYx*r=O)(LN?3Ta6zpVFyOqpJ=PkdwdT*PlPhrN8q7 zS@*64u=#t<>+vHQeN4^z3;cphOjX1olRzYsx6+Kn+fpJ4WLyrn@Fhr<#pJ5Mgs^>0 z6byD=EzKQhE`mYLJ7Um{FJ9m zf_hMF657rl9c5I9x2*UQl?QT{;{eT&$5`3wqlB%kkPSfu;}w5(gK!XVCGqoDtrp?#18(QYNgP5ona(D+wet(TYX zOf%Fs>@7uxq4m7T&t9-n*F2ozg;~r0Zt^E9%@L^qQoFWwdyZ!*417IH`qJM5{a45r z)?1@#P7-3WUazrkUk9C1G^Ebg_ihJuBC1bvkrucvmKL1Gwnui`E+cTz+zDY;k!-k# zX>E*P4{}_?mz?mmC>;}!F7?S%T*BVd#b1uAjTpMtv^*MoR~8_&P)zx;*0OH-~rXMCwxEbWhku7W&a6;vx| za)DF2L#=;K_iiC-Zd~9<02DWQY5-(TE%pv1XInm3@A$o69voNOZERE$bNThbBsGmo zoW)n5lri;65bhHKZpV0m4Q$HguB?l0BnwJRBm(b5d&LWstzd z$ikEIF9D$#Q9Qr-ig=|kD1p=S)AeMez}0P(m_pOPT1;1$4zKPak;+B63-f1O_DP$v z%YCQl8F^1Y)GiOj9AjvUE*GWg7SwRDFS_(r`|Z*s-8l}vW4~Q0dKAh#>o_yn@9K5? zi8F!K+~m;z+yCtmrbd$}NfP~I+%*hsV^I4mRnMKutxE?o9$p#`;_qWjdBi3o3hw|d z?>qV!`@g9L_QChuL@)l0a5r7$==B|P!kW|{_r~DFH(7x`0lv&GdhhhVOYcX7N-A1Z z&0>%r6PctxrL^g(WN0rKn5SBKz;C=@oJBJgYYEWlquYX9Mi1HSGLel18@-rW6@&wZ zfcz%*h!@YjS0%u6q<57VOk5X>e_e^QNdH?%{ej(s1U<{-;_=!vRP(`WrgEfW!^3IB zHyII7;}Nrc)J53v)fhH66mmjibf2eD|2u&0RC9Al%G{&#tDN=-)hN{#GpBvwGLFW( zgBg4QqH=0ylm{cOY{(`)=J+%$~6U z@c-#Jy1T-FObs~;XH3`_!T3948kLG)WAom6^QVy3z}dUJ+-z3Y96S8&L+>|;8W_q9kR3_6W=YLY2Lq&i*5beHFCpysVv8v9%%_tSQ0NeH)|lvFD!Oga!=EMdY;m+tnM|p!czc; z5-B7X{9S^hnS?WT`>rVY+^Q+=Nho3>c&(}qup}}}mSAw=>rwNAr?%C6QhsB3*_9q@ z3>wW!#SIH`rg`ih&-&;>%t5<;7fCuxgvEVpXHbI2$aqjaD!%j^U{T#FGW?UNQLCm{ z`aERrOhd}q51udAilN`U*V+Fv)cWlXBqWL}uaiHOjt&cwT=>_04IR#8w;`u%EdfRZ zQ$Q3BZL{8&r~L0YLC?k)(rsW^e583MF75tP^0W`>IYak)uq98dbD-Q*xxJWviSBt) zLN13=4%G+$H;3x=J!gfJD`a1c8|jO-lT0j>8j_TGM%+1aRjO-(nF*VN*%i-IdS}Ao zvhX-IHx`OJ$R=W-SovR{Wxb{yLhUr6eQ6f!5)fK%5# zP9WgPvZO*fcG((jN$1aS-!ZDKpa5I&D~`TjtEZ)yRgQ}YAw$v?dn_(*nzQ^QdH!G+ zziKdv&rvv^q;=0M8-i|9YO+D!a#n zkmHx=TeNcI|90a%5mpmf=@R6Dk+PtAH^z6~Hv+*ifc7w8r-UQ=yL9h4d zbFXLwzeSf}Qw-Q&?W?po!M{}0znhuw9`5UZ(bDHq*VE3y=`76PbBb_&T(DuGo$vsv z`mZ4iGzY+sBV}bMjS3K!+M3~wy#2U6&JCv8XnEA{yfScmQ)%8saHNiGAHP|?5F37A zPT#e?!(q4Mn>u22+VgAV+Qzzfxhc?;%4(I+sbSQp*99&{=v80W+$}6NugTdCzy(BB z*bVBF@^Np!px))Y+**{~tAxOu0;&SgdACP9^B;t(S0SrCM+vN(Hs2Cz4r(sA1Im4s zc01tdU(Tlq8_75GE?*HjFSk?P2vL(Cle@dCmtooD;Xy_J?WQXH>^TN80(Qe%;=!E> z?aWB!E2S@UYT_H8JDr{vVlR&|hWMv{Z1We>JxP`%S+j8?32)^K$Uz;U!|X1jkNg#A zwEtQa>$J?aOe+YbaMq1CQ}+8=kga(vVfpDSPtTELTBBNki%)nDPqbo+@e&CtHrd|_ zpZ}EKrflqpq?Qd-K9%zx+b5KR-oG`gBX9RTNgxFfMxzezR9o3|T}KalolD1YR{LUp zn02K)Cg6#lTsh%6$tU$HJujqL5nwI{k|%L3{rk%FrNagndliYZMn+}OC)W>I`5vLvIi5?pXaO-a*ed++q9_g=@R>mDmY5E z?j@ipg!^B?<_qvc*+PF~HwP?Lm|$(GJm12(I6Zqb-;M0l&Nx#jppMC}MZ;TjmaF>6 zX(Eb427Hn18Bd@1W>?Vtl{<{Bd6KY7jT%3$*^Xqx6KFWP)p-kfg3Dyzk!8a{&qj@2 zfF%(Fck!;XSbQb&M&h*r`9-49`+KvO89VPjmCO{cIVGJaVgeO1lq3u^D2!56VpItY z4F<9A3`+LnM@>gc7n@6WPiGI+=ga8ADw&Q;%U^Hk z02=YT9s0Z9_xwedyL47~eFHY*Pb)X3_R*CwS7`$Wf8eqap)1`6ybWpsyZ^eS&eLE9 zEUB%IBPjrVg(lTn{~9$fRg;gAM@ri?RS{5zlu@Ah@a;i<>Vf7IAFxiFxG8<#{xKxU zV-mFjZ*T;zdfvKD7RA$}efe$-MGn?E-(@lI7eevDGTn4|J}8hf-?eRM94AuJ@r}7X zjh=#YGX@vMV;OY#)$lUi%&(NEoFJ6@*$+O!wzpQIZB~f1vxwU}1=RMv z&umT72t=s-Pq!9`0`bCm;({0AQRp07>MJU(31@4rAw2(U5vJtv3A$Uv`#31ZuodH$ zkG*nZ9n7(L=UA!a&*dlcxA;IGKUHzCvhd7CA!07HnQTaa`4K3M)pFX@kN0D}uDxoJmXk2)c04@Fd!mH+3IFudNA!)3bEY}b%&o2{1AT;4)(2!3vYRg=VZ{6Qphs{koKF=zu%_4vY=?<2JndXL=+N~nGy#YqPq)TWc0^cY?j!; z7y55K9F7_$pT|l=y(-6=HzB#7)W`O%!m&fu+kCSiZXr4wGb_~M4UcTJp|SfQlmJGM3lhthb>ImPY38IYZF>Me^y{;t9G zU2s1qOfQ#;>DG06@R>k^tIfRs7xOEPGG|+)CnF!q@+#K+m+}g40G!WyT6T|)nXn7nxkTiABbx&BJggl?LZ zvL)z4s1%d2+zzsY-WAAn%6Q}Yb3Qqdy&Qm*;2nBxYo8UoZD99gtnJ2wf&`N^&*Ez? zUn1NBWx~K7h^Xr60iXPZNwm3`TeTU!jmnOybnbIzucL%s?}7A?bZXx;EAYG&7gNnu z73sEIM3q!PUl_hvJQB*+rIb7nd7P>GVF!hfr^N0VU$n*7He8mPJrC|rXF2Y7~2O?Ge2AOcC|c)zP&0geiFW&Co?kq z_XhpZ-Qy8fH3WnOhOd591uJAY1z25~ersg>E>|zX5j=TgE#usD;}|=%nn0z8wpk|9 z(epMeY323y#d@ZG)a(mGj@;~)E;zeaS z!rzd2%tbZ6HE-3xVQql=v&0zGyb;-XsGP~rX&gn(f!YeBZ3s_N3-Qd5sj=q*ESm>B z*MHJqs>==JUz`tYCwP+>aQ$B9)20m{^t*qrzY38*_?)k?-huA=n9S9LX4P|4fIR=l=2D;QBtlqTeQby=#nll(8bu@ad*_FOq|<`Et1Yqn0hU2wsT^(F(3Hm^U7I;>-zvRkG#6{X1$Gr2(8H_PL9wde!nAl?Oi_{ z9{X1!iG$sJKVi@6Q(v zwQJKGdc#UJO$rhx1i)jl!)SnWYYNKiQp%&Z)Q0G(7%7i5FQbQj?c+FnJM+7zkzQ=G zH5rBt1(EGLdzGLUYm0|^`w&>5q97=G5C)K-6wrtfdan1=TErW@k4!yjWwpDVv@Q#k z!~LFrh~VMB(NImL?tRkHE7*EO z`cko9szUqxTVn0D>xn#`>r~J~GKa&-N)Y*qo&xPdjDE6(K3qXy@I8t?gdlj!;*FsD zg_sp>5Nada{^@f6*R+WVZ8}o(Q&OMKu4GVPR*(Ti1g{ZHuYZMmQJ=YHq*e-|r63ydkTm0_(nUd$Aw(}*AJ8!dy+b4{ed0)%RYFe5vlhms54by#|CSp{($r)V}jnn%+R>c z>Pnc|w{#a4BWUn>_54;m?tP-A!g{Kvd}h^aX*B__##?0??q9SI{lbw|_v2*wE_=!j zFDrv-e>wxGIYb`XylBoLv$)qSvL0dIF=zzr|Dt1nrUWTK8MpISa7G*s=G4xv2 z_o1ivh^$?3x;Mv#eiS@{+gT3<=}$4$&srS>Z%w@$u@mGrM(Iud5k}thyOK$?h5Quf zgse!tziS)}R3_9xx7%FvF=8ffB9G9}#!Q&sb3F<;JjYb#bd)y69a)y@BEOMyH|PM% zSKYMfu+!0BL`+^N9N$)x!rcW19NK>H8<8LiL3%@8A@f3gC8var^R)l;#Lufj3|;h= zMDeYGOva1chT_v@Acz9l&Cwe+;x$AjwsQ-iRI^>IWs!(QmdU)Lj3ksknUGsLTVKsY zFu5KXSqaEQHO=G+@sjx@hrK5&>ep)Yl;$;{=B8(4n-fnZ1dye##_AH z8$J%2I(7Pqxnc?EPXtjLes;ZR!wUg)nkLV1fiy5Pk8rwP;E*bB%vt7?t*-OvQ(h-v zhm`26R)!y@R@6ODgG*?nq?aTWLKiu#r!oEYW4jSOEP8fL)h!e^uO;?^n2NYhaYf_3 zCk+?Z=)2xo+5$5UdCI9C?NMa?T1|(3%aIS+F~KBYo(ON2sgc-_Akd*x<<50KV!6Dn z%T;rw`tD1cle6DkeMx=)v|2EI5Eu4R!xvAMwynuO>g;?7v{x?ENv@Bip!4gN#-nE@ zU78RCXp%CBR$8sFL~;i{#{9CL?ER3V1U@$LfjjKCfNV&8O9PV;)}dHBH$BT4Dbv#e zt(}4gf%i9y?nA5)5t9wUF-?ZlzXJ4ar_Tq~7ErZE!}la!y^zWa=+ffhLgzO>s*`f_ zi$zp@L;DG~@408nRMYntP~5@8L5sux>$e4v1DHUuG=Yx>LHsxrp_3NchWoPSerJz{ zIm6q;-(5VU{M$c2?9v4 zOoPUCPv|&_H{NS+MCv-<`r)^OGNIh63c%cn@)p0G;P~LporMN-Y|7@(45%q$SzS_p zS`1kM(oHKH_WGiI&j#Eh@RW~Uf5*(x>nsj-nFI-bNcg(RV-@HZT^kKDMcgLZ?{DP% zc%8oqFQ90aZtIyAhRMkZ(m$kUKWZ~$Ba#4n;^*->YFHfrEO19)YJjV^xYTWTx4ebA` zLr;%cw(+9%d(qm;kwj03Ulx(FlQ{Nz>EQ457YA=rRg8^Q5h@67@N4k>>{x8r!Y=A5 zmQR93dzW}DI9vvs5j=!AJV`&F5pypJ(`_SA3y$)lP>p;+n=e$#i5mj-v%~$^XaAP| zr|#ytK2MdY0$8IWnSlVKP}$%T{hh`>*2%}n1@=iwz97NdLc6a+2?puf$-tTq^U zo3L9UOvGQSE<$Ii)IOgyRNQUEHv<+VzyC(>n|o(aqD7YBI2QcBjF*&*^a26kz3IQ` zyI3I6%3(FTAiJ@&AF+s&lOImCfL3*1jI86c<58@oUl-v6W8{W``Mj-AsWY;T*P{93 z@OUBSWJHk1E;Z?cZE|39`*!1*ao7;K0BYkgDvL{6P_F!!RWBnfp>Og34OwPlP7+Iw zcdnfp47C-AOG(jwbKJoK0kZw{(Ks;!{t|PC;W$ zKR?}_e7Y(@#D{!-;1xgt)Iuf3Ht2_jYIIi3INif zu$@#ow#=1hQq|xxN{#8**>j8r|7(t(F4hCuj4ZeC zg*yvy73Z&VydtUgE*zZ%lI2?!mmR$bceic+N#p{WpXHt&yb;2!T2n+@;*{Jk5Gw*I znHj4^6&DQd=$+2GKlxoVc2ly#R3fx%GNojE4xoA>FON2gSti*}F$7kS2d>Bd5pUpj z_nSst87Xmj**cHOv;ID>c&(vWz zeKn@1VK-s_h->xoJ2Q3_7dQJtCqtn zmJ9h-p_v{+^|R3%Y5(8(*U(f&6@5;q@W{?;rhg-jKpBrhfFd@cFW~@TuhnuNbf7Ly zDwY#)72Qo0W_w71Tbp1!ytqrq*cj$)rd@AMg~^QVgOEnl;ZuR3=Bs*Wv!8k~ZFZAE zDgNzSv)TE)Wz4^$T;>LvHt19ShVb1|)q+E?b@lDf#W6uMQ$gwmtQ?WviYf!_}TUnYk>VyNb=X6YWLi58LPflxLmx*o95nyH;R@9M76 z6M|4s9#JsrD*KxC>jpPgeb+{=H!Af1xvL&$CGrdp4xeqX1=(JFc-dmn>8YHM+t1XC z65I9sG@4w{^koqy)x52iY^@PY0y30qVRxd1x%$?5Y?IGp*izjAhTe$1iT8 zx6`d=>NobPx2PToI&goH_zQjHgY#{IW?e}sIaXD7h{(H>F_Xv`RmQ)S8Yo;o>k$kZ7R>#wvRzByB0?^+riWPTG7tr-Kb7k6546 zLe*6pCB6@PJC4rKaRZclEjjkIlJxNc3Zq1@5RNaPBUyNQ(7>r$QG0Z3ot@)T0<6g4 z)D3BKWPcIC;IG5BZT4#6OqyM6$|be_q=IZ7X-!TkiuqdUtq>k08YjJh0y-!W#@kPYFFa@e6c zY5|LB*sR$=V~Ge3$PCGD>~p=YPY%9`AP@G%hqi(GGqHlcJ}U$YjBw@or)6t*7!x{xbcJI6?@{7Ri?Px}!-WAf`gLTkzV=B7q_^3|ve_B(4AuM^|4(mj)gq&8C!gj9{fCiO#du=*%Od;N(nKiWy)W zAxu+GxaMsdfwV$3T;xPPoOlO3r6BO9;_pi&Jq3PBS!e6j9)1D4UD~{?pGq#d$=L8! zgMgv3_kSu|afRq;`jJ}^Y{a4?9aGWeuM)mBIKlVqAEG%gh4|pxWo{@{n2+sS=4y2Q zG!!zK(aWosx>fqzld1{bV;ibdovWnHWiTX%abkE2zPV2eYB5pP%;p4n{r=3CqL*J| zI!f8POAnBSi4N)|x}6*R1N3RPnYCSUAIe^1kxrV8OSc)pFvph(WvZ8ATB7P|uZL6- z)A^qTKlXeZ;XgPTp7pbmPmf>wwGimWm63xx}ksCqmDy} zu`l~G+ts+6I3kpJ+(~nz2Ft|O&i_Dy<<}s$eGXkjK4@WhU*$yjGcmhbf2qM-@ivd` z+Z#dc{v4>cg2Ze4Y<;x3A4Q*qV&*-CNpS*-cyY1pOBZU1#??ViwMyHXoftT->3c0^zbZg!mnP{f$mq`2I73LO1q``xUq}o3tTL zmJmGdFUzRAe1^aAf}@Z=C|X%ACz7w9$LbiIBu-61j*(+~&P-z9mKu713tZoC1vOQ= zAmP_vHyKSx=-wnX$fV=(A8ALm<7AP_Q)c{w>s;@|2NNj=4!p$c<+trA{C;{@8P++% ze3kw=V|4RN4-_)FaTM`0!WW&A1TjBx0_NiBLq}DpyIq^1QeO6mgd>fikbc*fj-jr}$FqW87FP zD)B@#NC>Ms84Q{HG!(h7al46rw14Cu0puqRW!VM>u%hQv?$K;24?N5J-l!fsM zA1O_pH+5Z|_y>FpCN+&fOPJP$hLbUc}qu+Jxy*9<); z2h$3WugW|pqwKhaCoJ#1j;@s4SXU=W7;z>Wj+L6*k6#GiP9Hf~J1s4LIdc%OwtRg{{*hT;Om=jIIZW-nCs9Zz zL4{pR0aB>qCHf#AuQP`Njrwks3RyhPFy?2pM@^YTNPDpJ@xZ#f`{aM>{U2y3vP!Ss?%BPRZ&o+~BP(BK(ZHvsuB(ua<4x@ehUmR*$niFRAHTvDe0v10mTGUpKY=XI~XoX7e+2L#&xqDKM))IJSdbl_K=5<&7*H|eJsyvzshy!O8RRb4zSbxu02qJ*MlDqBc& zjR+zLhu{Wte%ArN+Qwx&0er^dZ|ID-vEbT{L^nFiaO&@a4IHE+pSdD`l=9%|6BDOB za6Xc5N(yU*#p__J#@|DwUezod=e19OyM&*5^YHIpFR)`pDchgc=4XcB1T3m8O5%iq zZrG;J=j`5upyn?G{vF3xT| zy^td1!tu0*DP0DKU;Yz^eblE@Abl-w@8%Q+>BwmOcjIdq)|CB9pCh%`85Kn;VZQ^r zzomNT_D$I3`Xl#zKmjw1HS}vjN2ROoodjlk#mJ|7TEA&XDUsa#PTQxzCqd8kr73hf za-5gnuMI4%(#JKo>VsQogR#EF+Y?-yU8$I=meEQBGzul)kT>fP#J{qI_U7KZYIZE2 zGZg%88P8#XTC?Vd6=X_MXOaXRQNJ)LR+;LHCNW>M@m^dx-Ml(1e=#@&_#fiNLGOB5 z8lMH6-D4T|dsW{v)DkzsoN45o53DswFOa7aZDLgNw!3O@(1ar&F%bfS!$+4>_I;g$ z{AsQ|Q9K>wvs4EW9cp~;*3?~y{73Yz7~>vBFOv(n8_7LiU)7yDw;(*{o3(gr-0a^> zXKjC5FmS8Ks7ydOvnVp@{{LaG2Ps<6z6G5oA9BOtO{Rv$*lAgvXR^|$y$p;PPqD4_ z4@%h#BUs5Qw@RZ=kggB|_zixoMsbmHKhBl%iZaNDEJUA-EzdZmIM9ix$aNi=pMFv} z!a4+g`ghg}FbvmUtCM<>AXw$TDPf(#c^q}_{v>m)pPUq1Ukjst=W-FJB)w;&vbX+(IJ7uB>8wvYC)sPA$@1xJMX}|y zt8~ADXUB6(8#G40J=Fl6GT#KaEV-3_x09u+ser;l(EHWo5KlW?d%n?*BDlqddKp~R zn8cy5wo)B~573m)9o8JCAT6$&^TeIbi$@e6mPLbTO$Qr|$MdTJyFial7Qf#{S z9&gXTvcqO+Q3AS|v}{utCg|Ex;VT9e>PYI~EP&V1x;MtGfT6P;=XgTW6aKmBUd)&*T}bY}NsbmDVa0i~@4oDYHjt_uP_yG6ZL#;pOG_?=5qyh! zY_TaFc?W_u4YG}~2gP2~uo%)>-# zSNzf)29))#irYS#n0czAtdc{?_Zh(o&HRA)e{eCY?-Ag6kCygXw++BnTUE05659jTEfr3^^W^6b=UzI&pZtE7LCMLW3h!oJt{ zOer<1P$j%oXYNNkkB?buNc>Pz1Xwc=>S-=YV){Jw#fL)o1HRME@a9qzc85T_EmBwvE`$!MPQ%D>C$% zqD%Q#TxEcK&YJhUiVHu|+1PJ#E|szh9-!bLpfCP3Y+5fOQ95zBBVk3W{$b@8)-aXf zmGQ7P&piku?5y`#9}^Vj--U8EL#6#9iXm49)IiSLv!BqyF7R)E>s~OZs{!>I9G@D^ zYk0za@x$wa)1D%ujjBe)Eu)oPyI4%@M&Sz)Jk;X{DBJGeml;b+cya~kwy68wLfbhd zE){b5UYqG83f)?BN;=2$TQ*ST*m<^SkaUS#M3{qj3p*qpf(7Um(tHqAUHq^mN%P6` z+CZhpg4q`Z47fYY4M0|q38z+KPgvcG74_WKb;}3llA@DuLZo8kW3hg$;!Y*X*>jhBMFaCb4*xgj1|rem>rROyPV&!nQtk{NC@iPI^XadT z6Vj{CX8PxcKqM%Y-I6X?*(g6zvyu(ZX=05%NE9#IBpx ztLeUF9T_g(sde_qo%Z2A;2q@SBg<+~A(0D0OMM9sDLdcQ`=7V7h_Dp0D*mHwI!b{t z2f24qcqCLSL zsegHMF#|Y6{bs>)k1+>SQ<>Y(SFs$jD@@vU(JL7ZTIZUB`2SKsPK)=3zuy+x%~FUh zenfK7Wq6j{`N~d{uA_NL7`GMrM4uCYUUIKSFhA9%m<|4`xb}35_S6|P_L0}Hz4Rjo zH?v6Qy?!6v^kr2EuYSj5mSjx-hf3a(t?Ecq3 zYmw+F-_0kQZ9-SpkO%O1{2>y<%HY0{M^V}cYc_;> zZESF2WVQ&;eYB3L|EOr7%eCK_(I`=ijuBAN$5G$Edr-Z^{DY@}?qGkSM+J)2pVa(r z1RFZk@rKPup1%{)SU{z_O6dsR6qm&oCFM(Ygk7}`6NC+e$A20!u8BZ_PI`RBL44Lp zF0vkQIwsUT8-dw4MlgS2A^ES~rIE2e8Y18LvS-!^7H?BCxrV)i&_A|UmGMNn(Yy+0V9dwY|f_4+F%2hiE1=eXO;s(%YVqZrCAjC8- zsX9S|XxoE?V{q7@gv7r9m4b!pBAfOR&vCjY^bbg|rg%BIU6EiC>0;k)(kRp*c;%0e z#14l&fcljosrXYBCOz_Dm=*&2+kd*-BxIYBPwQw(C^lzE7^kbXR&f=0;4Mfg{5B9uif`mYB`L?L~`VrHCXY*_Un3*HZ+5V7`?psC8<7@MnMkGzk(U1 z2nSK`O-$S(Z7w|^^K#0LN8TKs}Ua6QvaKcvo8Tw67D$TBPn4t_1=FNLVa zv-GnJRzvoDhCq&MQU0)J`a;2#S_S*L4Rui6**A}Qs{jodn_oQSbUpehD!Cy-Dtv#E zz%Y{Z;H9|2|;%T{QK1*u)U{B3XUs)JPsdg9g^V?OfcL^}-Y? z8H_lH9nm-S&{l&+jn>K?3|@Vwz9wL{PZ0tCDhuG}sD#Js^O$U)G=qBS-0qQOh5AUG z0FO8-2PnJ1)y?Bb0E+HLKCL)vn`--*j$;~1YwgOJhWFNe9xPr>dU?;{P zO7Hf|UKvBeW7TpmWNY+=3`mt*T%X%$aL&Z+ijV%@m50H50OI9#vB!xlLid$*4E}k% z2T2iCjNP~#@gXgWD$rStO^2;Nvcx;nT6>2$;2!ICz}$>G7QbPNu)<}{O17aj0_3&1 z)jL=X3w)M8eP8|_T57h7uSLjduamaZWi})kkf&$@TY4i%0H$&!E))yCu_&U9JP zLZ_~Ti5{Y`05-HVK2`WtWb^$0TE+ftg{)}!O2L~&%eE`_#gkY&9WfY1DaaSxq-_4J zjz3-Y$C%3We<;K~8xkUwSoCu~+YSN4-zRl9a@Aua4uk&c+X)fU>Wl4k$FBEA!DyeJ zo!myWeMrBd5h#VoKZh;cXtxU$^OyGOb6?Dc28}K_;dav zClf)1?6w?Jc+w27CC_TC;H}xO?R|ISfm^Hqk6froKs2IfAThqghOHS96!rKRKJ_z> zkv%-yRd@}mQq$d+TKl{NdTTsZV{O}aE=oZK(0v~H-+$c`t!=ja%U**u1K)yO5jqcX z7pv$3kud&Yf&zgv&1gm9|96ju(@%{3`g6@(8jCZoC~|I?PR{G z8BAbJPCV}CYi(hJ|DxYJDx^)t9e;qLTj|u+)R{(@Ywl9wSoEr%P11tSE<{kuzD@OZ zEZqG9z75?aMy;Y?%d|#UoTO4Xchb_%Zr}HlXa}2f52B$0{tw*(OURK~nolFKr|%nE zo9B1|nxY~*n6Rol`a)typB*2$Z#EvHksAg3sDts~P+V&z8Q8yURvk8)f9`2RwRml? zO#A33}K7 zUREsVF<=EF-s2*){493gBL|jM-nSIg4#55k2#}NBF)!g;r?A?Ob}hZGr_A(-8X`a; z2Q)KEI)cHv;=4bDx`?8>$M2LY@&3&^JRrjiQRsyl`6}pzcTjgr*w)Y=bmA($pLa&% zL)cMVy;r3SdQTK9I&|+xTF`4}4h}T+pP)*3_2`k=YB;bw#w?_tr%5N#a;UhRetPkZ z5Pxy4j|fp1g(?WudN*2wk$ zvG-PCaW2pL=*$4YT|;n}0KtPp&>$hWOK^waK9B@=*WgZo;33G61ef4WaF^gNb3U?u zd+)W@{-29;b#DGQ^YHLV_g7u@)?3xp-HDqVd=67+O{@;3Lf>1xE*$&ceQ~VA(%ruP zSYlQd&wu(ewY2z@l*9Xth~GMh9sQEn^H}QvMvx)SI3yq^s=nkrb`cz-@Ww43Lf&#L zulH|4)54fp=yKPPWAc)cHxfbUq0@4*#e-Q`teaO1I0G<>&L7{x{vfJ#bK4 z7&@-mF2cOQghn$L16mu68gmy*>rWfVah*@` z+zl2kX>+%?-Hv3deAnBk>OxM5`d6qZ-6+wMpfpQ6<-F8=vRW2&^_hs!UT`$@Q? z{q$E36zWQrknatFb0J?gS45B3tF`9q!{n!!e@P)H^oVrd@%4o}?+h(Mk^sjY&kxCC zIj1ar=^FlFyd4rsv>rRV$9bwY0!AknT5%MD>|FrTSRV@k3WYHaT{IoDr`^%SDJ)T| zV(R5oUMb>XGYua--fHF@7B|gJgS1P7vJq&Z1AwzZ19QH{P%pUOiW~_npb1QPuhraz z`zsB~SdP(tfnUdCu?B}*UWy?Vd&*+GmTKMd8i}xG9K09vkDC&u5;7u1_*i7#Z_B-w`&-9pQ%S+UG#mU~2AK;NeoS%M0Gxm9K zHti_F#9+z+lvozIC)?~-5z6awj}^77pWl_G~*#P|6tn|F^47E*;7%$3lkD!RKhTekN08DFy-T>ba)gNa$WzP84pO%qf3GE#y$(w z-Hzf5GGwy)I?mbG)vulweZThb3{Y4bVj79%{qYxE!>Ood!}!XUjRqX1kiJIuz)*S* zUKISc$jXRw7Q=wY=@Jq$cihn}!f!7reW}`TO$mNbIoqwNU*igVS9_QI_3%q&L*0%( zxstD{sG?2`5j7Xn8@_<>NUfOWWlFpE#O8qS-J~*>-8!I+PsVGycATGkne>)C8e?`V zyu?vn|1~Ht~T8h=gpR*3< z>BRc7K-?EJPjx8J9Vupk7%v2Qv`3r-Ipn%}Ny#F!Yp!QwQAE#7YZQc$c{fjS z)qET9P-6&Mv$Xpj9XBJ`9=9lUioCUK>HF;WpBhy3O3Q*M z4gH?iKEdiUFxq3^b&yqud9==6drCdBY$8sJ`^FUR{gdU=^Z~MqQMC63>m783`oe7P z8ZXwI1D9Pq+us=RW4I3L=_xK|u-d z45-IeiLfbSZj9^ysI1gJwV?0Q8Px+%LZ1?iZ^8|Le$b_{u0C~5H-m3TTNN;~sU)^_ z|DaSZ2KV4%p^=utG~|Do86^zm;93#x}}S zDd6V0%7S-Ox7GcZp-h4h^*EEJEIucpVKd)%$CccWA#riTL^s77^DiwC?Go6ZgQHC? z0v@B8p~pkCR|p>x5p1_ZE}#!WZO-~*4B>g>vc59eJ6Fc{S}Bw(-*hDmr8vd^5-pC& zg<`FAXoI1~ZkzU+YG~#o2Z5n31@b!lllWmp|IATX-|G71DqV6Bkca6b0t|*(fks5PPAXmZBe$oD*5YC z{==4d>!U6|aXwbh)O0C+4SM?wpGqamLfA@%iVp{eP|BVcI}V>YUiTaLFm~trt(FFO zWBDNHGoGpGe6RUOz@cQ*+5@hoB4yI?`JpfeW=M5z+*wbIj551*^ zTpG{3zxyE1Q2zbBzuU5ek%d9p=&Lolo5*7wsxrq{rv|^b={UnYi zuip8zVh8p$9X&b#nL{izhv9SMlP%svQ;?dCP|SI@<64#f8zC~7WLcc^bKu}>9ZsAV zE}uAd48_N<-bX8tIbDYFZoSA;eT3C3DO>?(6~7Xap?Xa11ol8mg5bPbs3|Xb6j6k6 zM)2lRN&wXU#w1Mn!7{|Q=CD9+*5Vz+y1W&+PF6Zw8k}(p!@H|oZ^w9jgF?z!+BzSw zvzG*%5kF^Gr#gOL&S5miGK3qHXxQB6hT0JTF6sLPX^iO7r9BCJGWYr_-OzV5-3pV5 z*9Po5uk;&LO16QlHShhjmZ)iyx3_y_c~mpz8{a>n6*kUiGW%IR`>4Jvv6biYH7J#} zjULJ#lpStuQbO`oCVt?o-9u^F`)rC*D>_mZhJc$&y&OT@3Y~h3QuI};;ZIp=Rnm#P zE|belvSq<P1uE7Dg1{Xx{LxghcQi=+3?`|i`&0GZxJQ?(08fz9oSeM>*yRP z=l6WSF#q|haALtQ@-}{_H7=7w?v;_QAJPC^vo7H}b@3Q+_Cuf%V6YM5IVe0I0~ufQ zBjWEVVe@0U(60Dbjy5@6@#=^6OHp$lFQy50p~~>#x6{)Q;4dwV`#c``o~T)j-M1;D z6tH3%5j8G@;}vyC%D(gc*`*xTa@+D-XERUQmmo0b3k?1pd1=mx^z0 zd^ZawWtcaDG7&uGw*A`Sw_;JbgNWKuq*TTvALm^B13S)It1mYU9L!D|$Y-xZX)(mo znTke5ww`${cQVili%|WMKo|t9f%Tm5H>pMmIWOUUiGY2cu`k~RHZn*Eva&5+gFxjU z(D)dXDwuJsEmZ1(_oM?=MU|}RXm-d6)KNkTDl)6*c_{stGent=Ygf$+wSl{_PE=&! z1w)6msA&x_hwl$w4miJ9Q>rT03QOpLk8Kt6Wnh%O6V)TcH`YtDXN`&pdu1ke)f(9C z-TRULJTO7r;fK9;B*WbH)oJ>r2;h52vPdasU)eWv)hQPUUDa};O-%&b-$juISc;uN zgm60fO5$g%#=eSm5IW^WY<)@val*RoH1@;ZHzM=9i7}KtQM*Cdcgw>ly>D(7z-5EY~Ckgxz8Zh@sx5l6#Z zH>99^A625x8VDBnrcr1FC-&7T9tX=AW7>-pBH(C#VE;*7odycjlR;d3$hmTyw;0=AgFOD+9f3tLqsxo_BSg$U_syPD3*f{PwSd&5 zbbzExP>^IMGvFc~k3(l;2SKl6oaxy*m34XFzAh+2JtZd`zo5QeM8FE@;vrcy>q8!7}&e`f@B+K62;?$!#EEnsu-6~%mWtc?=C{=S*AeD#C z%s3YxlLE^Rb*r1QOhCibh=^S6;Mn)hsaW4Ue`XU(cF6Xr;PSHVSn7t|jFv3@a1hH& z&*hEIfuj`uPLUyYdE07)guleuHKZ6|=Kt>GH=`_KL5)$~5kJvI)#yAx7Q-u%t1uTcyNjU!wYMapv$#kk>bx{c|EyJ+S8ka5FQng5$VSV5nEMXw?zZ67X5sE8A)eL-HpVy7;(kY67yqG>PQ zc(Jg({)je5r8~IrbT@UmJm(RQNHQ*}$-FQMFo2=lh*I>^9FLD$U{TFl=6xKCx%0ihh)Yk+or-RO5YL}5^s{-^Gb%;IVC=Xs-*so8R7bV3m>+F zZz3Wu71a~`L5x`dG2jZPU(*j4VQz3G4RpE^x7Vp7!CNmw7_Ct}F#Xa+rA4&yb7N9^acOR*4t5QW49Rr2K@wRjKI5H(^#rJ*XL-T7N|tui_)^Til7k%Y|wZYcqT2SEdUzeSVnHV^*pRl?9->BD)ZSRzWR8mT1%8h zJP}DSA?BfP_p$M43Rw8LRCVN~vN($c>gYESHv{qAm{Q926d>zpcazt5!S_gXwmq}+Sf@eXezKY%;t4`tbsE2WPEVqQ z6V;Am@{;6X)vH6y`+bzpWk#x~wkPD~raEb&;@}EDx^+e#1*NP`9N%*NN0|_uM%l2& zE|d;lh&!EqeS@}2Aa;~c!IS#XbM?7h%gbfHXX;N(G~wr}AN1s2^xi=!Uh&m?1*aC! zyeHN)(q`wtuYmin^8eHJX_CX2-yA5+uIwbUbke>ylt2o}!+8e^ecxN? z9Om8S_`Ei^>_$zWQI_2ZSc`oBC>hU ztsHst60U}04$?{=4|@;t+9H(~Fep8w$14<=)RPsT;phYB{B~o>K~*9@?2e~;nEJjY zdn6Q7l~{k(%#$fI!rb??Y8XQbZpk$Jl&5S*dsa7=@WD5VAjT@(eW|n}5H}5@xIbG}4NS){1i!H2lq`-RbqcSch{mpq|b^Zii`$;=a3cLw1ql-Gt z5GI$AfwK+=MXc`y5;33k1`y?Vx|5NwIe_V6xU@7mu-h+on~GX8Et=P7K3MhRsb0hj zX}6DCSM@JZkm)F@ZBz7dHUTDfUwyO8Lyl4O^V?PASE<5Zh2A;(R9?_Mkua7e?*n!p zXD;y-ffiv^nWaT4DvlMczUU^zIU{S0GmQF3ZM;VhDa!0sV@~k>7F0mzCxc@X7_*MW zRJ>2}(Yq?L5V3X8m&ao7*D8Rlr}$G^;Ar&Om|1VaD>3w>2NvgQu&KX_`$yIrh^U z7LZigbfb7G2u%b{$Eq+q_PQJeUPP49_{7%9AW>{i&uwtn5ceCyShtc0{`zm^lA^*d zj^s6(QTl=1xX>?w=9^d!cCq=IR;1;&h167CXm-J<@@|u7A{+ZYG4CUJD)fkmr%9lZt5oBrj=j@T(ktZrh zsqbhdMY)X+#~qAzQ$uOwQv?n8IPosp(6^pcmOZL{gQH+cR>fxs4DuS@;Ip?2i-vE7 zKJLBS&5`MOwQ8wPOg#dN1LmzPyd@K^DLLNzf}hl%tDWGbn?UB-{P}g2F5i12rEo1> zvg&@>vbyu=;9qdU%FFw*086Hw@UMLUHasanwVgmcdY2g=;Iz3f+bs4ig1al)3y1xOD1l$90ujkU;z9RiF`~KWjGA+((H?_hRGl}&A z)11raLmidtL@-@L4@_VS?p=N1kP`;XN#eiU)=&(K4vTLRmb z``EmJ8Gm4Jr-FClEOl3E&UlXxcUrRRTi<_Jh91`^V4FDzpD#|0GVh4}yn`bm4N%gJ zrkpZqlx^_fTep-QRiB+V6n0tVStMn!jkM9TOtx;(gS!&+zRIT78UM<>q8t6V{sx$w zZ|BDSyhri{;pI}3veKc)X_~@u$kCtV5cqV{PBj(##X97R75fq8AV$EZco!uwyMqrN zdFiUIwvg)2nO%Zx;?xx@m;Yp>qPZ~~=h`AdRaaBhO`L9BKGWxNPbFe`1-%n7G!xjN zv-;z^1$1v?b=tDzwz9@8iNrFwGPx0oW;PX8VbWb=hW6UPHD!Eci2Irt9`9Xi=x_3t zY~?%>!{MSF{6{3vxCEM^5k;x{<{ff8$$B^p^K0uOeI3n?sQYe5#aM}p6t%&NXp*Z? zGw5b- z|4Y^ErZ{__d2$zmYU?He2p`%14I-Wx#NF+7vBOFOanJYnNxuf7liJjv0!9+~Ee^Ka zI0z?ym_Shgq^|f3HM~Jauk;))7nrYAm|-vO=_p$!&f_VdSWL**V zOB{rSLbHH?s!f*3bgMJTqqB{+qplWx-6ZpG}G0^!;Ngf2-EI7dnyJhMTS$Y<{# zk$eng;Vsjamqn9m_lG+2zKQiJO--X6Pz*rym-6z2zG#mQpe!^k%Q^UQgRMJ8mbuCO zdKDqqQOdJ}W){=uLhnwg*3M9M-~Ww=h^IlF`bE8MP!(lCwUn)Fg+X*1Wn!acmXStL zz$AtNu9H|SH9`EB@{hOMPnYRYf62ul#u9bBacN0H4}{ zT5XCY@FgeB{g>AT@}Mt~ahNi+nC>9DP|1$>z^4OFcg<+Zy`084rOaTD{SC?|jxWpP z`8bsq-8IaZKTh6;H1vkw!grPE!zpzi#qcR-SHxY^Pb(lIR2v_bYigujcQNx14%*TJ6;kaWsaVYbvA@BF;Ev@w82UzOKQ@5gA71$GNXIxTPmvLa3exs*O#!+ z3Rjd(x9TP8N2>Z18~)GJ&S{Sq^&NA_0>5k!E;4RM?1SzUF*I-duFltO%X+L!w8e}F zMfQDxx+Z&Cp|F? zg9i|f#zT9TCJmAxt_wHE=`+tY!n*(K-!boTFD+4~38wiwpOm3YC!uQb%S#7}eE!Y% zMZ}B;@rXQ5LTOCDm*yDGB!Iv9^s$8Hi=GV$u!=chZKeG`!#ECo_N0n3ZqxS>#5%!z4&c}frfdMr} z!;@M}eyC8P!;mX1h411(T#35++*9TaB_;`4(R4LfPmi+pq1Hy~ow#V6R+M_QOeRpf zQpC@PnO`RL=E;vDHB4a=;!uGd7n>+(ugaeO_S{Ml;PP&+{1NS?lTPQqS(|g_;oZ|a zM?8@-T=a~hu6vny&~p)Qq0_PEd6=*u8ZpAOh-&F?Hqqnl`egGcdWn%s=;yB2#SbeE z8u(+LkGG!}nd6te3W{ZBjC-HPCj5Th#B@ z@jB<2>@wm6-02J1_68$6`#AwrzxiHcUtcD?Q3*p!%tzQS=<%|g)KbC5;BVo$a&&;< zKr8B)N-^d#BpR_s-0QxPwlLHl;m*RNoc5{J!Bec(`|;*$@a-R|qoz1vH!Ee~I1Xa4 z{5BI18O(bHIW4_JeLuc=i~AY)(U-;Lo~5y+qZ?1z_O=Cno7_IIQ{H7*8xH}vYE`I4 ze%L_z!5w~J~zEVOXng*Mk&HCNyQ8BTUQw1+5L1nd{e`28`TXz|8 zCl|38qupXK1fat#7~02hj(udP-crRba4W8(@OC!1d}{o(g8h&@r#y!)a8U%8AmmWE z(h+;N;YkV~2q3k;M}~1h!X5<<(A1_cscP|DNU`5cj_8SLbi#i6x_o7>_+~CD`-!s_ z`s?&n-c0c{S;-)x9LFeANbg6m$3QyhTj6^~xiv*%uhPRt2*I1~=IDEt5N&@}*e2QX zv@li6Z|J)K5~}NROApt~Et^vU^w80HrMV3GRc~0pfJN)pLOzst}63ZiTbA5_u1h1LrnU}#S zLWfy?i>PZ%N6&y7H5qY46lnd29vbIzfL3F>*B9L9)I{L7eL$^f(A;$t1<_m9h~d9% zwldLBn!ql~Sg|k^ME$X}myXl-GEvM0+=u~*Caga%>q*ytO3aPFlqPo^@-`sAig}g^ zVUlp8uPEFg$V^*J$WIkq1`X1OjCzb*Qq|EUVOz^F3c9+eW!!SKH@6}#ev@n zf>V)Icf!YtH3cht?Q#`Rsn@VqoTk9-8Z9dSpZQq2M~G5L`V-;cL(sXR!xoy;F7zXb zOqOt0&jV%1JfL;RAp@msWpdpzlaljjptVbr4m@0m^(2dIcft8w+A361%cXyS-NHX! z04YY>DRb7%e(G~f`%z{MRiWi#XlRE1{4}r1MC;mh$Bj z2gh0D%i->kOZg2g5Ppsz^hqh2?z|=-cpAW?QYLpGcYylg^j8y{Tgc;13|CKXh7Q&r zB9g|&?a@}-`#ZC#lZ+wcicxc+;#?ONbGX*ez)@!z=34$%rbnM;bSqA#{&r#9y}p&( z+dMWI1jXvh_hQ9qJ{FU!+TU#~;a#0Hn^l%(Fc9|oDwbJHo_nBY-$`uqsdjGnMjKu0 z9=?W>x;aqWre;waNqJ@_(WOL@V(hbZ7Q4XL(j6FR^ zP;~Tr_D_df`qiOiT$&6)d++(|%28kQA{nuvkRFAr&sQeReZwKS7q^Jpw_nkd`#orC z#nOLGM~{318wq-~HfbY+$+wXVn4RmQ=#apOD|%D~P(}q?LP>Wk9#TCxccE{n@HKPev_$OzEdylWRl8ldAIgc=Wh^NBu!UB=v;mcMm> zCpYV7t<_`PdJIi5yt=q)qnA-eKX1bNecKvfT^fZ_?P!slIY$?>UXbDpteaF?C}y~(Il@9H0jgS#d=la$dAG1`x|vZ1xEPWJ~4vO`T}oZo*sB>*3I#}lukZW zt9a0-7w+enFt*_gzn9C9o7+k^c(Fn05L8SXATEJ=X$rbr@1T-HCq@lo7l-5wV+WQ8 zzzZdA+OD)v_k)*2U1UEGkbnY_8&Ntk0=#}~aS+Q)-Z0+ha=#e4u%@uDU#no?i|BiS zue7$G)#4L;ApE_d)Q_Ytnr0$7*qAPG&@ZVy}T;*HKg+PU{Q#15^u@nBn+0P!CXFh zqXj8G&E%4E4bOGS60=NOG}D&Jm>l`guR9fJ18?Xn$3;QJI^WD09$Ie`%nNBdV32!R1UKH$zh+OpeePOa?UEnC|- zs2hGMapu{wgn3H)V4KSAZB+ZWhyg!;y^TE^viv=Ml9_d+H59K)iwAn^yf&zvu&hi; zaLrRE{c4y5tx)$aw06wC%q}KDUKJA&CloDJc7XOCd&a|Tm(I;QXeYd?q4zxv^Hed$ zxoibikfZ?KB=Ym43$No(sr^c);YL3TGE46#K2(+ayOa(PZd)n8s?`b|_ssLg7mM2& zJXFmO3lN^;VrA)t@8FV*I{;Z=8Y8VLGmm%SZ;>(optuQV8`D_~(`7L_3B@b}-4Xl!Pg{QQ3 zC3g$O*gz|5R4SN=nE|C13x`#Q#eT8p1!9zUiOXs%Bp)0461jKp@F4=jQL0K1D#u0c zN_z3FVqgm6gRNKVI`K7>-^l9Xd#7`hJCe4FZCQcUf_eVFGH~a%!tv|?i%S^%rmb|K zh}}7$?V#@|CA->b#D3;58QlM4IG1X?-?9B-xg6o*lsaEhKGafA(XV4`TCOmg#ih+p zvfx%>-SM;-wLZ%3Ln11jGCrDV}9b!9N{)ig_P}WtWgQ<8L%WLX4 z_TSOq9gBNBV$#Yez`*JpSEObP4z{_RZ|#lvPeoK+`=7-^O^mS{Wu=r6`?eRA&>&v) zu@sT!_Q|O!w-*LmZNtC(Y1&VR>nM#(PvoyIcEjos<@khdKP*;edn6I<+Z}x?rF7-J z*cx-0pMUANy^ZvsrV2TUMf+v8Nuo7+`>RDdGEw_%>w5gWJLOU)o$-) z2hd-VSKqM{kD1$i(|E_0qz&v`X*#Ngrbs0TL$>D3AGEwb@LAdF7`il6H@>z*37kiz z+a{6orZ5ee4!hrU*~-Hi^VoRBWn{P>(<>(d0wdyx|LrHxWBB`Afm*An83(s22y`(^x?|G%m3Zt|9$!Y z70dtCmj4e%M1>3a%Lly{@MYNg9136sfm;&h2)z*$3?N@m8FFn?ct{sH6 zSwGUDdSe412f#+)9o|yD8SK)N6M{k&Abx7rFSgt_0#66U>Ui(KUaV?n*qtFh_14`N zqc|u`)YK3o+{aQgV%Zr|DtxB^>Uh<=;)h+5&4Ht|dfBaVz9kknv`?h-No6s65(@3| zIT|Cl7pe>{HMvDhg!^dd!BZyT*A!8rT|%nVOh9`eNE0pC!{$Y85$hfFO z6>jNt(dm)7NL-siG^P9T>$n}_L`RKnHW4Eo$Es>_e?RZryJ1<~)adkP&siT4=Py0Q zXT+0NN9=LgyN-QV>o4MlZA=HOkF@M<>KwsUZD)H^G_phG8pZd`G_4e3#|_CIp>&3n zI~l`rM%=0vGhsLW_QLbFJLxHd9yD_c3eAoecyUuzAHCKQm5d8s^{1!hlJgHbyA*pd zQ`pFp|3TaEJODKFJjYA8fsW(ILK<9Ww z?`s~^KQB8xC^KO{sj9OuNakj_0mmUR>tz{UB=#sbI^LR}y;1_{nRk9!;j1#MU%lUtoiMjsB9}LEP)AL%hSkfc zGijM$LPHsv24)xBIxgG}$M_)K6NJ^q^Kp!a;ihUs0!Zb>QLg*LVBexAPGF}(3wf#Ph0XwapqJY-s z?;~B0oim|8F4ZnEhlV-|S=f58P^EJ*A(5d}8s*&8K^c*4zoDvBh#) zUd{NLkwtDO@n3_FvZflK19C>~?*E4a1A59N4+Jmaqa%5hf3)OpKfoi1;_ShC{Q0Q7Suye#v%DHf0e_83V1Sk*@&++|Mimpye?J&pejut zR?YwC^?&_EKo4Ay-nLTF`pZcFr{B)Z;t;A7rq=PvCXqi~ZXy5- zOyRj<@V{GE!J`7dPK+jUlm1(8|0qccAp8G+2L%Xi*24(%ld9Rf6k1Dw6vTgz@`K!} zaj3vJ93j==>T&zJX=vAuDKcWX;{WDBR55@Q*KpVFE4zfB#`JL+C>V_?PuiZIW-rnN zy!*E_C;(E^gV52U9hBy1__>$HB2}N!$2ICr*jlHz&c8YUg0zX#V7#R{jIySJPkgoG zJuxrrLbq?Y+)k5vIjo2F&+dXCH}vA@0e36?&BPgKMS0oLoN9x0Tg=etwY6WfI{5#* zTl^(4@NtX>pp&RHHZl2WjASkI(gJJZfM=$lj^1y%0Hyq!wuo~+h=LM%B(=*T#V#SL zW+0Mq$iI>tWCi@UJ6QMc_ktjuF8~W!rs$e}4(}o}=9AF_De_Zp*@1jcPsKurKtVoE z(}Mr#&Ff4=62#9=F-2kvFr)89`Dw*|@Th@!B*9A~IYzJ@zHkTIKkf&5cEj=Tt^5On zq?yfk1FfFA+k z3Q|4Ue=0W9D@Y_D7TK{>T#KfG1hd*nyVxQwz26hoAm)Ea70`{fI1n4?7{yskDoNAh0Cqu&XrRl)Uu zEB`0q0#?2^;yQI|Uo*EyM$DK>=4Vypr?wD3WghNZiW>0w)uaE#?x~a!pLY)6cfnfY zwe#<_A;!r*=FGKoEwowD;AgsXpv07y@%Z)#hmBeT`2R+aOKzZx(4qurOeY?g6KcAV z7PF_W<7gP)Y+mXPLD_u@0>o@2qW$2}{;p)GxS<~dl4E~G&HYl0l~jx`=zD_4Rn<>= zQ#i?^2m$Jdhzb7*t>Kvf8eKj&LoKS*XUo_dvM4$o-hgxLKsiLqWZ3=(Gyig=?}3}Y z3xxad|K_*&R7$gJmLhEDA1iX4U@5)Xq&wc zVk>-&ot>W}x>_pOKn!x>O={k=gu1Oz6@ULE5QJWWs+w-kjBL;JDfsiu zL}7MBv%X?1JiU2OtwvrZRe);*YXq@9L=k}mSvH!5Dxlp6{R8it?PC5pc?0W$BmLU; z#4{rc(zGuB#Qj{`e9l*n1JusNniZv`k9YFx+lDfkFLsWtoo!sDh+vbgcTeQwaE7g2 zD;jdgRy~7mb9me46)$4S7hFP{CSpe0I{;sq-Xvjr74?$8pT{9SSXFP;ZieViWQ_7Z zs=1~K*sjC6^Tk(f^Z;!ds@@6)vr2ABvsk;7n{|=iVRn%1ndv=B(=(5ZWsL`r1fylr zg!HKhI$KLRR(Le`q^o)OHm}>`Hct!n-WYc5+V9RHpnVpFyQ*=>L1AMW=hAVsKX#?u zWBPrcnHFTR8x}E!|Hx<6J5|=|JwaD)7pAq`DTVF%)1FLY+$GoMot)G1@4Qt?kAL=b zuvgf|=` zBTtR8_v`F?&ksR__F_TIrA~{S@q~+Y(JT4%KGvYu{Cr`6Dc;h8$2uZ;xTREeEqLa* zrPgK>;CeQxmDJ0H!wIMeXCYpK9oy;9y1sXQ$5X1BeYW3C@-o@G!wawmN#m|q$L5R@+B*dz0$v)HM0jllla=-I}2H2yYp;J$I5xIm?L%^ z)K_u?Z}3{VN-edr(={qbtD|2XwP9CZBQahVkAYbqsUn&-6b+v}PY)T`mJu4g+t7!p z$IWIpdF!-uRI}b^UJ{r7kTnOuDDaYCx|!MAce3VkibWzgb!7Gi+B|PY5ufJ$YXE-z zeT4a5HcRB6Sp_t?03r|)V(XNJ2{B##`_@macumyBSsw=&!W&`sjbF0$*4F<4bpS5t0__F) zT;k7fG^ST`$-4JzZ|B!4b2N_C(~Ez2aGzui(f=S11dL!^VESalxAQbsiA(FAw&HXg zzP*JLaHPt=0}TA%8+g42P(Fg+)RAsTskV={p3Wwig>Rc@C1akuD}ja~IZiQ{|MFVl zNe}2KlF11d7*!MO#S_xGr&ls7$C$tc-RhM7Z@&d>)r-iZtl1^+%oYpmxUjVK1ujxp zQP4+a4_;dLwr|1tx13rZTrZMpdjuz3%Bx@4`XFw5L&hcOu5W=%3DiNswj^II@ZSLZ zAJAoNe+mk6Q8ZYD3XR#fRe4FV^pG@5UH1j5_9l?~V;{S^LQ|J$Nxp22=iM#t=D{Hj@0`g0RnWuK_9&(kvy)hOW@J5gFYK92p6WKwnzAXWw-v73@W6l`TI&W!{hNq?wPG> zQp@e6A(K4pFSOwGtQAoPo^&u;(F zyBreK2MfWn*y|9RvG;G&F{_MUKSy;*;Q`Ld*(L#H-V^_MT<;nl5D%J4L#9H(eZy?=&4;SW*PcY#mUh(e${<@S4?3cxz4v3}z@5K^NpK+v8u#2roCUyw_tkU-w4chG`20z9_|e^3Oiu0PJ-D0;m#qLGj&8L-wbz z7Q(A>GFw@xpW*t}q_*oaeQI31qAV{T=vANzb1bcZe+28;TxQ|N&vdSuB~|1l{bK1e zg8UIyQ=c%DdR4>2#dVbOccs2IB-vBLM+Mx(&JFovc8jZS%WtZhQCL$_+yz;k!zb^C`@#&$zC%(>O|j9<=n)lt5%3{$ibJc^PxkdALG zdirvl<5{kEQC#FyRPY&Pe(hSdKSd?G3f>*QPxH^pSS%W3U!$v>9#9-simR{r)ELdW zPYUdt&kIU;OzR7*!5{lIEDuT(xQI4B2CG5d3WL7W`qgH<`SaH9d*Z3cac9g3wp!`o z9AmBeFc9UU>H03t*&3YJb9q=~5*xnV_+^PTbHA1qO(>{d7Vi`$dc}Pu-?x7wJsh33Ro>#jYIm~BMB$pBJ~LF4Ao=!-B) zFuzshU3=Ss(K7ZR23sS4sB2U6Zrz%C96EbT$;5fF6yAY?@95;H4UF~Ru!&SGBqkkD zTTX)KhJ99gtf-WfqP>QU30#hI)z2&5*}<#hEXI^m!aJQ}BF5R{fGLO{=Ka$=X&iBf z(TP|?BgX7$7FtWAvyBdwkZKfy$1L~+k_jdlJ@4G zCWF|)SMK8Xli~R97~l&u2aNlCj7j{O0#8559)Aklyls)pxTbvmc?Utj!v;M%cwv9z zxxuQ+wHwd*_Dv&Sd%~ZP#F>6rA3mErp|wEw#5_N1P(6XaUZMTU!JzsiTfZWmcpnA; z#2x=Fqiv|)nVdUE`nn0FPdwlG6k>oOOfF$ikhmT5I+Ou)yTMHd%ESW$ZnH-jtXy+D zF-N@r!#OXDBg^eHpe~4}xOMdEj=&y#cyvbTtL0Xf_1b^O&(2~eX`Nz2ab_770DO&? zwtwSq?F2aIIk&)Ty5;E)M8^RQEcT<@m5IM;WVHk=balB!p1$APcaCrK{(zB$NviPn z8ZM;p`vGx}C3EVqKI$22-6pv=dCMS=Dpg z4<$760=5ff!x5?`voK1cn6+$CUa4Q`o=s`F zd5q~%iKlbg^?luZOp3_$?rQcJSnr9N@maXITxl2@3GC0@=5>ZKb^HSPq*?Adl#KJ! z_6Jw;{M`N&%MTtqOHpA)bpC7gQ&NMw3#>9R`TkkyMq8uJfTKv=vS~5K`{#jeQ3mxSHV^^;~}zZE20X!`MrBd_8YK z&l^Fa_N0-Nc+h1l)x`rLxuRc+VW{f7>GAoom9%EFsCmOVOTU!k5~3<>v+_+~fD$c?pk*DZ{wd9X4m!7UN~3fY&qNvYuCXcOa? ze*s0Y+k|r8%i-0@?kCo|OzK7V5q^EiX8h;mz*jI?fm>s@89{r3A0z8OHGKD$YuGyd zlrA4bbk9Y6dNgh8LkBvCFc(PEX3s5ON7NW(bdM+7xQ81m<1tsj?~HK8RR1U@nkJP( z2AfTF{+<&#+$g$fZ~Ef==`cGuLr6*9UwLgTU(7~#UqWO#b190b_1NxfDmPD{LP|i3 zsC;$uW23pZf}+u>>Za1&R?qhnU*#tthm!f+yfwJ6QiI-5-I zUOTHc_5-|W`y%ck{F-5VaaZzGt-6SuVYA0awf8z7a=Bhk<)o+m0HX@`jdPgWZuw8# zZ1Tk=U_}nZcLdD2E(>E2Fy?_rgVv_EgM6{Id}I%bug=pqE9j&(N~C_w}6o&!pypm~%aS1qhWW z79PM|0^`|mboS1s%qrBMXx#1Y@k@KNiQO#DHQS^FgxRuAj+92JIVx483>(1?)SpeP z>(%EewBNVBF;Tj&P)drX0X7)=%EzUhaVzHhDtkYjq-vq0D{AI~`Go91#-+)D#XvS= zQsT*9bUuhRr=oiDsunceRio}7-+PXCr+)iVxA$Vidql9@30rx1s5*AHedF$Sn!~w5 zeLRax)+S+0kWa_I+CDF>wmu!w^8w|-baIA~=B?o8N(XFE(W_Mp-e!J{jN5GI?d`F6 zBmAqqZ^{c*=kfl_+a?*IeA4{;gUo_!aSPvY0-FTf-D~b%6-VK@RC%e2YHgksS19>w z{)%y&d=qmG*gRoTV`Wod>x&(0R|6#HTDHGtZ(=4{=o~96ra_kYHbg|JuCweZ$8V7Cm0xlPq#7WLt#v`VrAWI9%WV536L7 z0^fYNYs|8fFTGK~Og5aQ&oO9}b>B+b>cl6aFsiKWi7zM4Hu$Y5LU|U+sBA4QzlRf%0;NONdJXyhQ0@b^)w{;`iKGgC7 z*HG3X#F;<(P8=M;(}Y=wXW7oF(~35ER;9$A)B|xn7r_E1zz1vLi)>E>3}9RO`9>!4 z{fa(CScxhMP^I5QP%_gl(4`TxzYKu5XnIs5l(TZm;@Kw@7tM&{EkzqWdr`nCU`qnK z4H`sV%S)D8Q{>b&o8Q!5Zs8~)()CICfej$O4Pxxo3~+Nhx!G<04hzDAXy1}b$sxnK zJ90M%)sw}UogI;NH-15q{GXW#EC_H-`j~EE6ZcaYc{V$`s~LMW-TBiALl8q38ar zpY_~tJf_#(+r$*aYtaTNXIyf6dQV;nv^$oGUY86lRPd=<B2j-FR?^`!Y#dRvOFS zFynj{o86w3QXjw-tXLVMAs_mG$odMXrr+;>Ktc>a5Kxei5|j`n1cZUo9STTGODdfk zp;7`8f^@fZ#~4UTO2>d94cllo7;O7bzxDC=Kj(coJBM?&C+_pQ_rC7yKJTX|k!wnm z^;%J*x&YliD6+N};#X%puND|x_iefSccrhpG6=9>K3n*vY~x4Gtl&?<;i;3%yc3Yq zH6l>OW5~~s58m=t5m-|)MXVk3qr258ekA-fUz4L?CeA}u7R1zHehlKn?rA+x_!EnV ziwAEd-#V`X8&`TNkAvbfPgv#VbEzRSNC|aCT8aupE4ndBP+r7L9fhRaxzCg zsQr#g>dS`&I6P3h?H@8ix-9OhezwYZc=^v4=3JErsDUrF5$76QI8MtHJ59sqJ+PVX zyYr>7ZKucXM{wZ%c&~%ZLdyBNMTCgIb^LU5zBgsK=oz4&mDgGRzDVW6C%TGu28KS< zInajURfJKj^@o#gJgaMvuL5}Y%T26F{-0F5yEzD~Mn{Ab2}&LqljPsMd|qq~Ki+`! z`+zH3nn;6&+*)rqQqpYtzYHi$#j9EYc+J}7sqhy({@XT$8l6?T7q9LlKL6V18~mueJ6WQKa&hIeakn^iI!1oB2N3zK)?l@B-xhC;JP zQvHgA+^2EiZJZH$s$EF0@-5oM+Y4NNN#4v4eRkcGF)AOBs4pa!)}HpoHdK8o&}{-wuf#NEV;>yg5?TEA&pO2Gm?4 zXK|N@11W^i2H51sL?16Ou9s!zn}3#6qb|vg2qN@|#4|uzYaybwQP#nbti!7GJ?}t! z@xrd-#Je1Q-p;RFyxlzwEoXLaJ>ihDuGnIa6gh*LzcY&^K+MgUAhEYl8Wz&G;Vc}t zm)4hfH7>Qiaa4X@t`%!;Ohiap)4THKZmT}M{9>S8_&fl*z`AbgS{Zjc&Nv^8%*`XR z-@D~ZA^SYV=ZWIvp9ua-5X#2&wZ}@{_#RjELFR-Y@pHf9rNRkv!R;oW=grL=Ww_eI zni@%PhHy^vVX@11hFbDH+!qk25#zHtaKdq2$aPZEDnl8~=nt%S&TbJ=k%XOVR$iEI zX1s|RyN3zAblAL^@GrCB`^?*UHrU$G^1h7@DP_i)lC$j+CxHPqKuc(!UeWx;s!zzI z;B4@zrUym(s1mPoQ$7}i9qN;^9TE>QQyM<5#8+ZH`#*gzfAB9tah95R&!n<;E5bD+ z(^G47ypzZu{C3*dc<+YqIx4xn#5-46QPD5#=kwIdU#%Dhs9SP!m$=!d*C1b{H6d42 zn^EhTD5HqW2kDw$w{Oat9+*P794ug5p z$~1IsazR(i5^sWMA8Y)B#a;SMP1kpUq$)%=M}ED_GoK&p+(4qzqiVgOsa~JFV-`6K z{NM;Ow<@u?0F_F77=bJuk+b~G>BweY@u`_I&+^W5E*k!|{@&OKxg;uQjr(>ceczt# z)OF`mnc2%taO2*95^zFg!jLNY_-VOZaK(^dvdtJzw&^-MJ`$($0P3s>e2G3E(P!H= zs-RNw`iNCWH?}(oD>+P2Q5s#jY0o`(>klkvUi~^fKVZ@tFn15>ShIi|%!E59t0CxH zavCt8NT`JZpvTPGBk_D3I`HYFj$1YsJ=*pTY8bbkkhG+uNpamIaJckn z&}*DjmRCfJe0!InK8dLM0UlfT10T(Co%P9D$|t}1HHdm;3v`K*??^eN=&6R&)))NM zVagT|{G=PkhRpg`eDIW1j5L&Jpm|)2meXvFZ|~l1M@WKkUaP?rGGFSTk$LgZvBzq2 z6(sa~t%V~+eIw3o_-H)h$r+bdkTv=+WwmSQvOFTu~s0f zOxI@ACe`);dxnII6nobJFoB~QVCuQjcm3-^qZKnc?hvuOKxHKjF}mS8`<5q5bi~zk z_z7oAIg|G5Sbg1!89l>i&zY{9w} zWU{Wt^CMSdW7pn8p;aePzv!uI7fVwj{iZ99P;tUWbZg39bxY&+H6DTPRB4Un&IIHZ zxS{G_4S%Z_KyJEyGC@k0LB#A-O?|mByp%+J^1svL$hZ!2Bsz=NMTJG zq<-2OvKq=M4zC;@9`BJJu!$C4-i45#WPUk#oWL2-Ncb3p_M}k5zu-Cj zY@Gdd(D0R-y`6`k+%DOZhnMQ-aloTy20-6$mWqhPSAb?*Ed>WVyNCEuBYo(1y1H-g zUoUMGGjZ+`Z^#%l8vz0|o=pq(PTWf`(>=IC@Xs4Qij{#|GkLBH8w6o=-JRXswCf#I z$ue$r7t0#S18$`#yjGkp<`c_*PRq(YZNlwbO)6=5a;flJk5o%B_q!?n!kz;;a;*JUTvlujC*CWnzZ9hF3F90oo|}xb8&Yx4{E0Q%ToO zmZW{2^;$?DRIYxR))E1!r zMreL^QXH z`G{zV&ZpPRerID(D18W7OepX*la=|2or=ssf5J2moD%=NmQkG-AJYFm8G^0ZVQlFV z+VcF;lX|)M!O0c(&xWml~ol0GsjAQDy*^x^i}AcqR=d&rg{CUu#bCJ#0PI%c>CaNhNEl>@hXf0)eJ-w z%{I@7e`}!F?Hf}J6Os%WiI69^1u!p1gqDVvwBq*Gu?YRc^m2hz>RO=zQyja_eA{?E zZMaMv^PiTsR?IjXm{uVWDw_q)gOxX5iCZI=A2lkplB(ZN z6eK1@uoJF_piu7=K(0G@j~d6}9y0B2S(#)^zP${JO*Unk%g?ee@`2sbu#G;SP0gj&nby2qs3*DHrg_$?$+ixBhL6<3+U_hjOjX7ZV~oG;P;+<0JZ+s zMHswo%v_=daXoo?*Wy7NRv9VM@kC@45S=!+swdcI|Ys3r8-aHjon2 zz=!!F+$^T{vB&uAZrtNA7Z#zz`P`i7ot=w;SBy4O*I3FTf*OZ;4<>#9lY=h!tA(!$ z)^5Vz*6U(2AFYDf6k2Rf0^XFxteb#*u*aV!>X~Mg{N(NYP!vLlge-c?#XrS~cYNAQ z3)zb)J^uaYOJ-lB1&+QS*Du|#Of4$|>Ygfu$xuheus;7!8i9wZz6D^SCF*DGLU|kI znG|b~^9X^%0_=3d#Eq;nN4iv}z{xDW1TmtRh~M)`BntjA-;J|g93`tU0n)s2_krel zEftuOWTwE%zk9@~(DS%R>^}+&sPZ*RvgcT4FX66c{c_e}CH_NEWjr?b1f@&pJUZ;t zPcC$#k=Vw1ZSFbcGdr=*D7!}w+SJ!gD<|Lso?Q7x`?We0h>BCLm?O7N{p-P0rTNZM|do$sFSq(Ok z{R3nV3X>=34o~iVYt0+VAi$Z`I&rz1yk2j$R!ZA!sQk)~cNGsU_J??54y~ zSS^o}uM*r-zZp%a(yD4X?(ym=iEquEC%4T$g#W2f@y3&=NPc`pk2NWVd)A??sbP;r z>a)Zr{0#?`{WY#yw*+FEly{AVi$y4dLL|bgbp+6e`sP`Kx>=!TDg7h;cYFolGAZ}7 zRJxf3X5bHlZ0b6L0BCn70sr#%f8_PSmi)`K#AX2hhGui2I)H!wrR}5o?ZCav+6Jl{ zzTlCnb9fTnkBYCRk3AB3NIrg@shbJ{9?g%|Rgcm63ZMWDl^1Gh7h!5y@a;|Q#u#by zIY|R}^~&d+ZBCEJGqYDcWuJ`&Q!+++Y5b|*Uru}*%;hYkuMc2zIZPUSJXg7wQhUxM zB;XdR*WA-H>tsJRno29EpWUiO5YjX)(gPz-JR`tkRsuvzo>NWiCgKaQRK!b^?|C!$ z$79<(m3S*r;wp`-U*4Dg{r&2}qFy@31Hg~3*!_C&`D#!B*jYWn<=EqPuJCc_#biw$ zcO-}qKkRp!vOVRPI)>pqC}#zY^E7a=_0d>IvA#6@VZGx45Oe?un4Nny15=OpTrrBP zMwy)#E8-IbP$h-`W2Hu z3O^Yc{^RPWxw^kVv)mn`XJ~T28OM7fCG|NC!#hQ$)JbSTuuYK*dh1Z?0_kVqtHeZT zWZ8ne<30|rg8qR52;&nK=5vFI-a#7*NG$DHK|z~L!?d6sD^by(_(rus z_E;_6JJpNwC*A%>4d!~yneC(z<*i@NzUphy#%AYD6@r^DhT3I3@-wpluQR_WTtOL3 z{J`8h#7(#~iud2(P>EK9Ay`V`*p|Lp?%z`K zV%ZS;S{s49dQQR*Mu}XWNu(afioG%77^N6Pz0(2Y$z;l!jWB-7MTGtl#KIwRc(?HYM9;8hEmPAyvO3@y7}jl-Q>W5f7QA zuM2@}KB)X+n12CL-5W{ciTvi!u(IZcj&uV0=-Iwu0RxWJ?VFx{Yi;!Y6W??$$&M9b zDWWS*6sKR9j;`8GEs3b+e?Owvq=Zx$;I#Is9(T?(*niL2lGV}s1E(Z6c=PDK!q+wZ z)1*5qBHI!t_#NYBcm?b+upS2o2_sC?Y=6%Uxk(oixrq8}Z=>F~F@$G?Z=CqOf@NLp z#xx5{&RdX)E`0gCRYO&#xq=t zRA@0<41^|)wPbctV`Z?0(g&^^0uOQOBDF|$keS`TWn||!d7Y_H#_>NEQEn>KumsP* z4eAEqyjU|21~D2kGplAgd1jMqQx&Bm*gKN2z;Ul74KJG<@AFPsB(xN0(b9n|hq>1sghfcRHMxw4P-=Ze#k0sy@vqQw-Gaqp%DyWXFyrMNQ{UARhox$-C zuu!fQmJppZVfra!f7ETT`W5m2W&<-e=kGf4+V_Bq4;&AE`t<4e6)JdtzMsf7qC5`g zW$9kRFR(6myI9)c;=#i|wN+wLlmHF#X0{dfC7mad(%o!iysH1{8{7!CHdV8sEi>An`5ZX=zD0v25S)rqIXm)Qc!7>UecDMncf7 zca{C{0RiKL<^n6lS9Yf>ErHb)e4ojjUDT25rk~wG)-KkW(!Qe;hhNnn;iv6@ z-$rLwGZp@AZ9=3R_Xrnd1u9Un%$!2&m^e%wyrn#%fQx>cVDZ99=29WeyH`qRb0S}MSu~eMD-M8gSlO^x>tTHs^xg`c(Dcw08aAqN3*nps4275u~WT6Mkv&e zZQX=TM*8CGC#Yx2WTIb<4%<=M1cEBF=ij<%5Z}W=&BuNpV2R?CC|?3`s@d**ZW2ev zxrr`b12JWkVjrFi0Q(AytX;DzN12^5hRzsQP#yRaUvMpLc#FTsAnJEMB0{zKGS{&kmeZU~%R#!ohQ4&pL$0bVftinuoOrB=uRz`MD>4 zdDmfi#aj?{CD*j(1Q{LmebgFqOXfjQw1NN3!xG+-4SiopjE@@H>EG&W5O?;G@VN%s zS;0{r0(|LHpu2eoJPztCjxsm)u4zA22jk3@_CS!8b%}Yh4wq`jPhwa`As?TfY?e{ndPh1TV1F7P9p?Z=Hyd zlI(X2MNsyl?eV-?U6Y)U(>H|>YMZ$BPA2y!OYylldDYs zc1a+vn6khzpRee~WlUk6`!8kOKAP0> z-m&D@EIf(vsJ_@DezRZ>)4T#|@6JR6sQ<9O1fK^o?$9DioOqnlS_5gw@M5j3Jpnpyz!&^zLr&oqLDCCYgI48!yWiV-?Si5#_B#0%E%CHyM0H-sCbVn3G2hfd z2wJ_!fmc3lW2N&-j`%<) z0?9ZNLs_n?)t<5*ST+5Hl zB}v@1m#&7K*cqV)ueR#xwVwcIZ!}o-YE01q18 zfMEHEOuhL8WV0kN#oFxayLzx^U&*WMkC|r$=ITwzOb6|xgbk;I?L!YgMP>bW;%zfK zR`+mpA?MCcOrL68nm$ldOx>8tncso&9qfA%y8?i3S`(Y8L^`6ml7;}47R zq87iFtMN`)Y{>lFe8~i8{iJ49veI~sNJ85AndM+Nem>`=Og&v)iS0(a!3g*d;{WH5 zZ19yA;uH!+F&WIa`K)R)=RL<`mf{k`VA-BN#xC*Xa;AtIf=hSgZzERlF{-P(9BV~{ z5b9zQi*0FaYVB?My1ue5cWh^Yib6Kme=i*DYAiqxH?wa(sYEw4CXgppFt*gW?C)o? zzx|_R`d2XZn>8wxwJEtT*#N=LQq>4qQ!Sicv^a8xHl}mpQI0kgWS$c_!+AZKdn?>> ziEz_T*OPYtJ8OIZc()rMhAD{lB<5vl8!G(f*tAovobX9wF0W>!s#XmDqTS^~ZGM|E zpB2$XvtaBW*6?4pA0!)m`74vI+OZSQq2}rBGdJ{#s9JLDl}hMYl*`3S4I%{9{nyD& z10tT)Ev=wPV_ap_`v3+)P(RuMs>0051JxE+VH9IxZU>1k(E`r&nu9)oJf-L?(l3ndwl!s3q_`1&0o-J{P zxYlvf$7#&ayJ!^D%;SLpOGMDj+)0X`$C%NobIoz$- zyg zq&aREx61NF_bGM;a2uj3uH**#{Mu@6+#H&7xA&XQtoHh`Gjm_P%%{X@zjb6bX{C44 z1J3@x`TqVy#$iGAMXey63WxU4=iF{N1E@_8w+e1%IuBaMOva_vwYt^KhM73+-KS5Z z!jd$m@Ba74&C(*G-OgOzLIOt=hI7t(TF$|(dV~4$h}9GuFL2!v&EBssj%94VKiA%y zBgjjNi}3XtEGl1al3%(u`Io5$lhE4dh9)?Yu!A+{PrUj{;mK_WM{~dYb^XbW8(k^J z;Hybi6WOqv{DZuVbS7LkKNGNoBm@^a-2KQbZDG~rq3mch*>bb5FqnGAtk&U$_0RFO zkEYaYUuOfg=6>cxP^ipAa$H3?kzcyFWV-uX_V6#(HIk260zd5?&r`CF^ zp)97H(#fXHz6%&V&g&QFs& zcaZghy&F9Nj|@=*t>*x2^Wi7TIjd=u%hAf4SRQcmYVSBFE#79g{PbHu;i$#Kj-UwR z|Bit$nttE|N+Hdz_-1rVZUh=wys6mOV$tQt6 z!SI&W|Iml{0Br3-S3+pY8gjK_X`hNkClof-FatLQ5|X^^i3Wp2p7qa^X25TLtaNC;|)L-*<>l5f{m z>_N(Dp1M?C%iYBE%4boZL+h!eabffh?MR8;wfic|J`+4OONAYYN7=A2g@fsXJsYTD~k0bJi5d zIPU1J9Ar(NHM%d#0N?!iD8%mF-)?TLq$8)W1cnCaN(9tmIXt&Z)43PBU?XTDlabH< z(%yF`+$XRKGL#L@VJ(tP_7JbZtQKWni{DC)YdDHDV*(QSx87nVtx~MI<0~A#bjX&b z?vSx)snlf}9nW^MfxCD%7rmbLe%CRehD-HL#w~5*#fN+r7Ju8t+Ls^+9Zg1-+^3GR z8#L$G`Rxe^GfdilPIX3iu9bUMb1j_R?}uC+pvlLd#wIn&q{V}VjD-M9b*Jr(XmfAx z6!!U06OSW%KY>u2O4XfX(=;UKZO&-}qocijA@`9Io#yFu-E}{85i|SZH|di)(*yV4 zAt@FlDgU`2L9)A~v?scG46PkY{OcYU2b_(ttqj}*@dV`S=gh$Uk%LFNbuh@L(Ifv# zGUfvY@-eetE0^RFXMJOhxo*}Gpdy3;hDMDqEbfty>0in9&W!2`xz#)*!1ieMK&8xW zdSvgQa(h@QiK!8Z8lOqL{eXUFs3yz7j>vD4RDI!}Q9uWd@{VoHu^v$-qeK_(^VxzB zvwptPZsw6M0qolkm8Ux_)^CV|&DZ)ZB*a=%$`N8R(+adiRJm0o1VvUn9_iIc7F8|t zNUY(`xq=_S=jq`q8t`pBJqT+8wyRLUjvDT-IJddUq6c;;XW^-h>6cWg0U8hurV|Cz z|K%?}8xy;ydWie216l(%bli?Hrh1Nnf!bavWP#k0ZCTP!3|+hH4P(@=CxN9x?I9k} zz|o#-gj=Y7Cnlel@fNjFA?H>THW!5Esn%tjGJ^5jW>Nz)ZgnwJ%-wnTi}`zh4M6!$ zv?yl*PnK6?i5*jaL_sh7ZAjVn6`JC|Z56N0t`m0g7?UdUqi$HiE3*Y`yw>^3lgCs- zP@wE6X*EAuh9NTj);G~ZHyUYipXLlYeyEzsH&*%P0flFGe^+z3P64yuTEDYNc{X~J zjvisigb!<@T+D|1qb6c7UJj^V?qzJv*=MEQm&Dq&H_mEIhs95N+HK4&^@9M~8!rFE z(Np)}t|3Tn9u9U}VkqXvR(tNyckiwxYUjt${Kk0g={KEGoEoMJH@giLGb(~n^# z;LYXZpZgfVj*uhYk1@N=EdX;{CI5Mtv zHGk{2ZzLogenWf!wD!asAHzOB$v&VY2qbvd&tw;hEQzOdJ|e}s%1V>RYR}lfWAQII z!)+YNs=t@JoV63LX3m?ne=nhvG@17FEqNI$!)g>iJLMs0uCy`7Mz|qrIpQy~C1hp% z+B#I>VpzOR;Z_29`BwXB$^M!_n3-wi=JDh}Wzz$^|3}gX%B_;&+s>h1Jw0f;=tH-9 zKHdY|H7ty-ap$cgbV)zU!%tiXF+Oz=dOL;<@eLy#j8l}m)=C`H!0n>T z>ySxJ?r)3=nqO8tOiK;C?+rh}vjdcB?#ZtH71iRDv!kfalk=}f*x5t{+WJFY1r{}2 zN2j8`$^^D7NpbqT>dr`hzezr5R>?k3E_dPC0!dqV6pEJ!|4x;DlL2*uASGV>0%Vy0 z7(K%^;fz$9hm5xKLe||Xv^36yLe~$?pBle=QE){fS->&XXQ4A9LR=%Ma?M}w!EwD) z!tjdZP14czzeYs!mgFww86X8ZY{4f~b26a83|@t#zJ-=XK~$XGK+zhb<->=p3lfig zyyocysi-aW(d-+ERF?U6H08`UwWmo37Z@4{ZP-`NFC+ZVjTaYp2Ov*b`TYY!Z>3#l zZ+mP=)jB59#ist$Yv`SXX`#NOk7t4+pn>Uf`z2R|L3%Ajr@%XqKA1dvVdRPS~p#ieXM)AA9v!MYa z*yr!d-4stsG3L10LIA;M08)@quuzB+@`E7N^;`qd@L#Sbh@BN7E5|@>L3oI#XKOhC z;y+H704o7ts!AxWJW$a$edTG7U)`3BqX;D-H~Gt`WM6ccIL)YW$4R?;iK3T`XV;F_ z_l}3!Uhh6E;eq|2j5{JJtb!l0xv=mn%~On-G05NETQMb6*qjIzw%3FNg%`oOzX|mY zbUFr(b+Dkrt6oYYW zp|`xOh}S&a&|3K7UdK(F0wL+Gd`2yp?tN#~R!Jf8S=N@a#(wpZTgWS)>A{|5-2V)+ zWg}?AHPGG#&1wS#`q9|S+(t=A+ToaT4;;S;nxCtgpSj#hhus$r))+C6`xL+|Kgvz+ z#AVnpL!qPCtQ`Kk3;rFUM3!T}$n1VXi@ZX9kK& z@(|TtBDMaf$rCE?Ov+ZE?|{mNWI?I#^~r=#Ix78P%T&ID`Sf*z*;$_1lPYFWe*Z$_ z_UWFz&@E|N`PSK<2cf(K?E+PEt~U{p^o)wU+zXdD8rR=4XJq(=Zy*mTH9QeGwvs{H z-S5N*B^Pd!3YTz^8QBlbqYaxLGOBt~KJ|me``$DYqykqI7Oni`Fyx)CkueFEirpM% z$q?1w4q5gc{ll5BQu!hM6|o6IRb`vz*k5bEsI6nLE+0O{h2|@aoOJ>>+tw{LX}Sp7 zrqz&ja|*1x6;Z!qCw8H?l>ihzfM<~MEane-GS_w+D#(Hwc?_ul%-x0tK?t>w_5Aub zj=Dty)hU~S?kk|Q4Uf~@p`I*L1L?_z;oKTnqT`$Pr%kni^C(702C)D!$zM1|XbS90 z5d&u7`{Uw}Ui!W7&mP#0T!S2KBWp*~CHclJ;mMs9AMwoH2tsO)2(g4&Na#KeN4Ya* zUCt#Wo_9xnUk7wXj0xQVZ|{Qcg=eM-{*-!XNQEaXS%r!wsYOq2|d(`L3{c=X0Z;=AA5*?_<0aLZ=@740gRvFoUwom&_w`@0AAqh=Zd zEGaI`{Wpp26ym_h()QQ8*PbV)E6|YXa1~p0ybr3#&b_5WN`L9=^qm`=EjuB(A-8DW z>0ApZxvz3lTb7HDo&BNm$Tjt*5_nnrX8D{e_CXE2g?4Ekh8ip>t=I+b&c5`#e|+5R z`g9WVGN7;SQ0z^lscL%CrurnKMs8MI_vVGF7uJ<~+cvb=k7Or_zbhY6PP#=jYYa+R zgc)RVr70_EIk%sL+Z}o6| zR+}-vb*SdvMVZ$HJ37;Yim0(@Rx?72g+Fzd-2uXtMCqdS=mRdi`>+X3mwm}%4sr}|pD=&S zimjUwa0hAA4fB4fX>R6{{d}NILWo-b`<&ATN47ZF)(ZoYMEbxwqHk6!caSKq zC4KF1`PSuOI;pY}d#4KflA@g11y28wSK{WAoa1?&)4Q=SF&WH$*K;>5zQ9Z%ng(_( zEd7!rAr16^BwC3;QvC0kJ-^(<-G)3N>nz24q^$)ybZQ>?Hdc=oE(NX(6t%#F^L);` zb=+1Jwtg}O-1Cx?S@}g2K0_lFkwS!fQ9W#z>u@cxCiaF!kqg^-Cveq^E!iVln&_Ef z9$m1~s04`{My);l>Da<}Zgu4@z@{U{F!V76i}-$BW)2!BdEeu$Pm;@*TE##YnT>d& zoNms4SRN52!jtrVv8vZug{~gbaw*_wX`ojv>@-a&^X*K@A~`+V(Mkg{PJkk9>P&5| z{VMFHfZ-+oa{C*1NK}Ssl&!Rc@m?wh0y(>I>xX>jSBM3PXAOWQ(ydw-VWnn?2;64w zn}EK@C#ACy3G!XfxxDT}a22OYF*`dJL zPolB6{$rPyF2`L&lqsTB7SR(`UEZPs>-7PF%eN;+*;riyXSTj^)`;zh(HEsK4tJ%1 zi9M4fpV4>E?s48;c#hvqY`P7Pd)g1(-ejKIUi>QFlwVX5{v8_6mDDXFQP z=)_{mMdJDi(vZ*HGu;a_T^Cl|OLsU*G0)}>D8d6x3D7tL>NIZ$iND;`R1wqBVar2p z_jPMdGekr9m`VkiK5hzm9A#T&ojw`^O%IF0%FuBJe%(Jgt@^$liJ9s3!dWC03}QFZ zQa0S_14I+EuHN)`O1P)&e+b#ZrjlKEpYhwtj8gFqs?3ym8g1rYcl$}fW>j^bB9|Gy z%HbecfphX%V-lcU&848J)c)e`80j6N5{_!@Om?2au#b+Xcw$W!4FMqmXLmvC!aOQ% zx33`Yr?T><`YDJ+;FswnEEys9nC8FUNxoIy>nBCwh442HSo~VHXa0D<)UdH7Y}#*q z8C1+|Ct&dIrGbK{biGb_=Ozgf9v=KZ5c-Wq%Mexe{AY5$7_UWff-&HCQ7xs&TxQst z!^4l(Zd+u74gr99^QM#g&9=0TsMXUU^M%YC?@7go)h&y3 z@zS2cRTpZu$-;5R7Pmp zgcp?D)>?W~bTOm81gH%$;WJ}5I{jkb8CNwE#eio|wdYBdG@DccCU7@=pXs*HK{1n5 zC^f;J(y&=Kk}m(4P*M@3ZVmou7r`PeC!+sKgd&TWPxP`E-PkuRq0{41wXRD{<*oaZ znMWLeg?bKj{`fuk>yy+JnT`3OO_IP5>*Q|avH$fv|N4L@C$1|xv1##sxRNEwj)hu8LcUkvm(JXh&~5g&ycoRBhj3xtn|H1wNirU! ziWbhSQlny>>`k%9jw8*yPchSE)d{tG+KdKfPM8#_Y87^TFDb*{1Kx*{F7SYyk5fAH zLFPr~FN{~NGG>St{mc?aoQ0Lbw0YVcyHBLT_Uy}AT7Z5xWTs4;UI=PGxfyx8{Xp3A zURikIU{tDF4u|S`Ge{e)MY3?o0)Jr&LY6G-W=L~~c3vZbw9mdl-2U9nb9X6$b`KfS zKzqG2VT%Rx-GJIaD-(ATT0kv0qbS7YrVC$tBko&dZcfuIfytI>HQ_sI+qObeYI}w0 z0tIB3|Bs``CkMe##is>Al4|GHgmllg8ILkeBs&4{)umB0Z&;#y4?13|72)$-TIYRC z0Ey#8)R>Eq86R1n@a5!-7l@mOXD$i*inOjVH(qk)Bx2KvlsNGP-lyxJc4A8B%%Vu92G|SuTy4)>rb+@E^K-a;?ZS zT9>gbXQO#&oJf*{^!DE?O+!QIarWD(Qt!iio`grH6-~<|h?4DnNBjQ6nT)2xJacJlLk;kIB?!52A(0t3$81~9ua1q07% zrB6;W-yN6gMXLKb00PM3LnkM{(qyU;31wls4UpA*%%PiHHUK7*@(%{3>_GN zaKAcQ*^E-|NbHBX0xxPHFn6pt313Vcjzb+O6ri?p)i{#jwFYQKxH{pILLmRi$&E_Q zM4x&xRSDV4KZ)N4UHpU`d^Nn1U8pLv$HO~)5mqh63%8Dlonjd7wY^)g{A#FOb)~kV z(ns-&=)kC`cQj~Yeyic?IU5RHn2BEKiGGg$*ifol?LKt*h?a%;S=2uRncz#|9ivh> zGKVy5%(Q2k;4AMe6|M7MJyxvEoW9*hkK2L%y3ZklsRPe=g5&Vd5K}M1xrr2tRQ<-T z=N{DL{@|3KfGP|xYR~MlJrIkfU?P?zqN+0wK#%$tayho~iaZU?35bXdc)-*%pJVu- z+Fd5ZvN8zqxO)WCK9HY!BT#uesWCRN#;@DsY^CZisT*XF5Fb<1S(9f%BWwvSgr44VETCQ*Oy#bp68_bg@HH4hfnDBK!*=qehe^$Q3 zVf3CV@NWD3OP_};@QB{cNS?76EeJ;gD5h9WD$^1Bp)c4!|I%^?7U_*%J9P?Q;!9~e z$i3TU&*4)oAEp+78rD`99(Z+4>Tc?ZW{>F0qLMu|u&4hh&@=CzhfK{F=P=uhqfo@h z(d({1r`#Imhe_B*hA#h1naN4-{X3pOz)4N|AQ3JpZ?_;cZD#_>1XXdGKWOvW}6km+l?DgF(_O5%qEDF#<6r{zGI-dda5Dw_n@QzuLf8EPf z7?m5V7Q(g8{t(OMqHZhK3pE))*2I6OYxb37<8Df8n z^+*3ZOS=LEtI_a{h5M<|jD{!;OC^d;lT@g@XVl9UzT^Fdw?`8^tgX9i6-%$B1^bQe zNGWH%6n1767Pn0z?|xaG-t~WGaEWw5R^K2n;mU*cN)3SZ;VRs~43hup?08jXbkx82 z;R$O0qKA27>Y=MMRy~uq+Nkc9(d0|S_v9NO+PXM<5;YgS^==d(9(_?ltVf9gMu5mCgr|Fvig}~ z&jqgtxJ;cVGJ#TPQh`(9={(Jpi(t(+N2QCL?5C&>+j_w~6Atz*pMQqRv#pnX?%!Pa z+-?fEt$w0~RLykINBB9tEAsuY(H?i5{92%eg#+fi)&SIK*?uO2S8P7BR@YL958}W6?hKP|l77_rJsaU_fvX^R`glBWL5lMx60732%E9Z~2CkA;1xn zSO}~aUt%`QbIqFUR#`pDT1+bPV|@J{Fs8ZNbQ&2qlb?|IMP^*5kVBDI zTZM7JI`%#p^+2OfX&1aF{kZ<>^hjEZmik#X`%n^m}-cLVA()JWem{pD{# zM8y0GQ_{yQa(yt!n5(@PmqA5kt>4F;$0rHithYtDOwq%>Spor&91wCq!HuG%NP&(z zl0lqNz=5nM=+%LFPmEO%|ClZ!(TS^gTqx2R!v2|(M{uA7w=6sNoP@v++~u`}3J|NC zj>EZ=D|mNa({=7kSt^xAC|1%%55NE3l_E`slE{7a^$LB;Eoh>9W($wwkC%N5?T=7k5cG>88rHuke_yw$sSPKVPuiiBD6S8G@zLHB3L20tFl>PT&Up)9N~e&%n44AHn7EX_fHF4otUh06#~E9O9>$5$C6t*7Y3raYVnlTCgyyS)x`xn?^`Ml9W{7uif&Om-YT> z`Y3A;?#fzIrkOPu>#}E6bZUOkv-9rV5ym(SvUHLnH6dyXHlNJS4%=a_eDI;LFp6h{ zPCzk0$Gkd9kUl!=a*qpEMg5mn2G7B^OfLE6Z3M2;EY9acu(;t(+S&i?*?Hq-i#I{v z`yt5(srT1yN#73;-p4JjkXI&eHOtBFC4u0~mdX~7QWQ>LPkiaDie#Sry6{N4#&wVN zzW2bC2Ai|zhl)T#w|$=48D>AVc8Z}@guTK=-#q2Ou)GK28~m4#k(FJ};>5muEW0~3 zO2&!h1Ng?x>Hl<`5+7_R7F(r=H9Q|T-S&rNi6ZMqfsZyTGD;gtzIeT7P21z%8%{4< z(NIvEMtKKc?~}doTI)*0t=U-?v3;Oo6vgJ)qf4o~H{wWvY{Xt4foy5AL@8>+N$~Im7h@gJg|IMIaqZGTk3T>@wnC0qh)68vy5FO z1YzR}_1)7yZLfw0(|?xhVV(R+mqKn_MXt`blt{3k*Ejx~j`=ksv{F~s<@eEulM>9ka_D6CLUVFs zi^M40<2&PUOMUww9OwdcIjM1ek>VGfw?oP?jP*8rxJ@#O{1`+9p{{bRO zjYMElrE?Ovs6$q(b+oFtnpHe(oHaaX6mQiM3wwC@Ihh(Q9wYJE?~HPBs07|y`f9*y zV*>m^VdwT(H+620VICXgir1U1lw6lLvbiaTyiS132mqI2q$vd(@U4D zYL6&nZQ>xRL}`~w8@a$0q^^~_ehy6x3xM*)sUWv)$f8d?;H`c zF`gO3FrjfPGiIVn0&Qg$V*nDGgo{q8LfI9g>8wtVezL=Y_D*Q z32MA{{pR7RBAy|SEziAze64vxK|R1GPw>ou%luJ`0>P@vEL^345J@GXJ7llbqpz6X z-Ht0$sO1Uo-Iw_CP0FI{D3y%b!J2P``2iCXlRuy+wDk7vneC{Gd+<@FjdWVc)(Z4; zN=BJr$!!BIHg8L7HIG6U?Tu$gB}`V*U7k}fI0tFQr@1@+K@W3Pp=sZ;;FwA^-zH#K zu}&NAf&Y)auMUf9>*Agvq@)F|G$MkuARsvi0@5JeAl*uLha#bLr?hl8N=dggL+8+) z^PTb5`@YxzzCXU_`JQXc?6c3Dv-V!=w_>lo&pvABtI7h7o?ahz{QGO^MAxe0i~HTF z!aTNTNG>mO`)b|#X_y9;NmjjCa7~7GWlWM$rRdmV&Y#^$UNQ?N&e9>J3UaU-iHm1& zDo-)P#6h8Q*SIj-Bb8LeT+K_v!@n!`O&k{QaT3s~X+30E!F#Zx-aBt}I4hf$|Y!e&eL-;ChV_i^l|*V(kv8GFWtkD{j%lcC#RQVL5y$@a zqVr|*cg*2^y%#50;}LmHDsyRMdy{qBN%ay@2`u44=0&OAZtEc@g#)TZiL&psNjE<; zt)Fu16`a9d`7&Bm-j98Y4y)87Lr3Bvx)=Q)IiyzaBp*vtiu{OPhjUxSh|Q0$yqLV3 zAHS@can$jmt#as4>}5Uo`pD;fxDnBbCdwN-9M*Krp>{BWi>H#h-6}S2OXcB&p zCbO61cMK@<8;ix2#+#zIo^6+Zt=C6i7n zUc&2nTx*U;HKC&Vftt~enc`Xg8~W=gv&y#|1s+4iBRj{a&x7@Ja@S1J31^6 zBW6zH6YGvcs5%FFp9ITNayi|;>oA$`(^?z2X3Q9vatPPkfaE%zn?bWlzGHt*djH5? zj4)1E4=>Dc&5WGl!iDX#A&d8+aaq)41!I$@RSVTUoICcPC$}DKykrPOxm5;&rtAE3 z*8?cE0_vvPhpn@W{dfx(1yQ-dxMOy1mG|}e@CyRq-NakuV8v(X<#JKyFKroe90Dgk z?0ju2gs}0af798@R9_mS=ZpQ3v`y*y?el4_`Cj{=<$#b&!91P$3rJunaaoF`juF+&DLHrLEvL6$6D7vQFxLm0@?Dxd6%f z+ZUx$Cw9t=tlWRyAbUZ8QMQaLEKeY-Zz7C@EcKrM?fg`xIrC%L2H9(z7#B|5X?0~C zj6i?tqq)Z~6kje3(gLn7tt(Y+-X3Xp@7N*Pj}GLPJ4wo>iJOLYS0&~=vIJwYhZ_CU zbf#i7i6F`8umVgFuS2;AU7!ZvCA`hWH|`nA50j=;9WAcVP{GS^)xkoV7_^jRdneqc zweM%7jo$6j1+bLsS4`as=mF6yl(}8m{A~aS<;eqz!bDN z&V;*suJ8S=K`?KWZ;Z*VdE$JWm?ixAOn!q0`TnqSsjTLgB~p&c`Ac=EO3pj%(%u#= zZp$I87F&HBob|HTQi7cJYb)zQUIWqH+m%%(oh^mVm`{wM?I_XS(Po$U!z&!a%BaKb zWeR=2g+bA-#5PEVT0T6&#g~mCktKO|IpU<`TS0@K!*_2-D)2s1@)Dc2%j^t#ZTR@S zlU^eg!6XIPYKeZ{V4L1ciDSEJ2%Jf&+n$kLXf5heXSU*+r^Vqz;&somDKnmcI8c{pk*i(i#F5nVo5X)^zGuc#DefUYHt*2 zjLbAiHwVpe5+!zWcF97arlDKM=ZbBT&m>)A<_SFR z==Nq#7f3dKQWhmWD{xZV(JVMv434WSyACXnyRrC$IsA&TP@A7^(6S&zL~Fdp;#DUq zyJXk^%!YrkuV)gHznZ}HRtT7<`11Fd`y3hL<$84`W0H9=d2ymK_cFhDT6EcEFj%})0hqL z7GABU#?^*z&l^$|AdhBu24lU?ZAG6frG3-99CLav-}E4{9o-X2>4;DTFXD(GuC4Rc zh!I_KM0V=qN;FSq!4vO~4sg-oNZ>^u?~Cp{SEOXd#DJ}$Xro!MwaCs>V!-ghjT3Q-Q@9s+p?BdggbUkfUG~^-`lt zk2@z|x<+XvmCfbw-SD%nl7W4S7dkU)Be^ajo8bhcUBT0_xkZZ<8P6T&*6Ib!E!cyfw+)O`Qx$%Crb z<6^!1Y>g6OQ;KB_ij%S|vR+i$ZFldxglOc8El4W?=96b>q2QiTp;EU5(8YW=hpgC z|1J$Uw6$>hJ8m#APhhG1jFiID)_3J-mjwHUfz*pABpZw`1OC7qb&k%M3HLIzhEr z$d4n`=+-+8%`D{E4p)B9tSnYX(a0mB;&`EMyeD!~&7rGQ zAzsw;48 z>LjaqB=pQ{JFgq@ZwMXa7Sf|U*o!*ekfAn~4;_g#)2?rWjU zLU~O<(J+er(BwQdQCotO1yJee9w*IlwwViqm5d-(8ncrT@3GK^(Qu?J`r;EDXHgH# zX|1PY#P!N!qK(1Z8*vYHOMIM&R8F$m4Dn+Kn`!ezwcDY>s#PP*AuKn~X_wY#}EF`F0jytJm4xdlxxLV4T z>*&>dLj9}Clw0mG;cQyAP26z;{)z?GZR+p6+MI*a;C>=i|5Aeb0X|}@#A3F7n9sNb=wr6pQ6ji`Tfp+I}JDn~LkiTbG#(c(?pT zNuka`Sw2hmVZF@rhRkHYAB@h1n|`MkU5QVC4Xev6v?NsA+E%worgDalt3Jf8dPAj} zDlX4gdx@)5rnKN?yW=WUd2E<_^JVsygevN@vSDRV{#SQ)viqF8zK*lu;Ben+DMBhL zT5epR%ca9*awYiuCy8eDTvmNwWQ0)2D2|N?%c5A)&DgR#si9p%Gh(CA_m{Nk*y%j) zNbhdM+q{nRD8^mQ6KPj^so(Md@9FCOmq6Y?0Tb8b!LhjF4YLE?Jql~jib)#fH}fKs z8>PXDWs~L?33%eB+@ax~D@N^Scwy<8SR@}PNJAZFCGwsut$*N?@KK{Uka)NCe5fKL zuk?8PHIqx9%0mb1+QwHIgpAti=4E^jVwDd33wR1d4fWKDlq^%HHMTA^k+)NFdXxpq z>|(ZDw}$-(*y8LJ%wzIO8FR&UB`u_W+le=fBjNbf z#?bofkQ<|u=Xkb>iI~+hvtN~;GDXX!+4dx*^i*W+h4rgiQW*(UOil+US+7YKV)bcw zHDl5RPvUsc5wT3hyRZn!uEMUln;XwnR4gRomT)oS^2=wMzEw3es}fav$3QJ+TvQBa zu350SqZQMvZ$0{zT&eOhlE_NDj4xseX2TPbf~{OcvOmPx^yoU5CTd^ny#2B-r4ql9 zTxXhXS*0YCgkzq8C_f%Q zG$EMuMCG6f1IHdqO`6eXOT6EaPAMrZJA^oI?>*eaJj)1b8}PqW0NipMA_dLnttvFu zBDpX@j&zevE{K(g?I|^u1ol*ZYLr`tN)u28%$?`)!KLJDqoWEU?4<(;_b>u3k0j$6&!OsVpiC z&$w7@nXz*Z#KD{r7yoQ$W-%E~`J%wJBzQ}sh|-O=vcrO6?iJL;b@0&5ZFxtH(rxoy zvC-byt2r*h7MbbT37@tds0lt;6|#92j_43~>l*w;1*&Sq|PEn2SufNQZ@g zZ{Xm3;Me%IfyYdWjo7=LJ*c6Is7WtgtV0f3~apx$WD?o7Jv4BwsJ zE0RXj=9}PdD+yB1q1BVDdRk5H9r)xc{qhLO%(hC!dV62*7LJzgbm$@4%=xD^qS21& z?S7omq3MWt>}G4^&k&BIel1nYc-;OYRa{O>KSg7B_whaN^rjN`>w!&`>Ust#D3vl< zH`|Crc95oLcS($mt>bG8)q-Z(v`@EB9rMNuy3Yuz=b3cz2On3TsXWbkRyd?v!%&#_ zJi}V+UKN|i>xoTOrhBuA-bMWrP7?jrr*&A5Y*Q8Srm8yS1@4w%BZio@`+eMDZ-3Fp^jWF4d%)v=b} zEbia=3P-sNG`qAmV?9ROu*}RqGtjDj{&K2XR6J|QY9oIrXGrPAsGcZ)$Yn}K7+fXH z+7gB49GlavE^iCj6DtwTIGBo>0qu@&I(`q30^2V^k@_ltVZpRVy`lDkb94*Iy=jt8 zkE}Gt6R$C9=WEaqT_&A7Rv$5cKnTmV+GO7MZ&a;b>Q85pxr?dzaJg?Auy=fJV)A%E zr{<+c88nJ#jwpNgC+UUfLXn~i72jIU)S~-!dOq%ZQ%Yg@zU%0*%l^)t=L8A|1z84a zK?A{FxGb~6-X?o_}tD&q8-R~6sm zJ_)HpkLm-7XU?%RE_}0uCk{hIP7{vfbg2e)MM>PML<);+4t7uc#5>JOS_@5gu-(#0 zunwmltud16rF)Oee9^sMPg6PD8K-2e+*%(*(S<9H>(yjHjf0d`u+dr{>!XI)n66_V)W8IFlF>tJO{Bp-p zG2`;p)LFBp;l$^efz+wjTVyj}?viO6&ySSVaHuCkA=BjDC-6rQmvyH@+OL&wl%TpL z1y!P34*W8yQ%agRuU3b?U8C$b{I+3I>#U}ojQ~5D z>N$JFhx+j5I^t8B%*}dJ#b|tyLqWR2Xp>4u zZzUI?vntX8 z(zE-a1*R~y^lHvj3{L|HsDTRRZU9AVfWYR5s50WqgE!BmQr@3uy>R55w0sfP5TS&x zhh2}Hh&zXBjc$$NI3bEGUgsciM){OIo7(?y_9VoB|Ltb29?4H%3@q}z4}MlG{aVL; z+|lF7YwgHeOZc00RJnO-DpX$LfWz)#p2$Qop_>VCLb?KH%E>wEBtjh6n_SO_h8OI z4@Hn5V!XEGSm+j*8!wKgyCDQBsCu;9H@ zb~6?RzUGXx_8;E zZV`E2_^0U3(G5{i?b5Eh!Jx6_D(k(5D}zT31@>>vIm;r%gM z#>V%8HvPSOT!Y_IF?9mYPp`>yZ(0Y#)qtuwoiD)f4SAlS5R_{ z>W(`mtgKYxt9YFg#8+lt(e^^DbV5`y4XuLBZvl=R;`TJ`l52#lQVQ05S4)=f)(Qe&qOXbjls!1C)ZT?jO!sDbNOh5u6VIkxElm~eXO&6u~ zB84IlA6*==z}*b+a)$3h2F-GY!P3{Zk^^#^O~37r{K0`iv@e1cZmTj$_XtT!hD+Z- z&&CNq4)S6SwU#Qeb3TWnc`A;{4aK=ku+Id!sL2t-dWNJk%oxmhMqW`K)DHGl4pVQL zy~q8rWBQGhf%IkhGRM;Ci&?0cyOB6@u2IigYV=ouKh`@wT;RLhc-O$Sb^a|hCGvFe zOT*2?+n0_MFVsrXwU6*emWK`$#!74=$T{yG6boIdT3vn8!SMFYoM(AU?(kgb*)h>! zjE7Az-N?4^FeQ5)nk=N9J|oXoD(5gu_wFeZx}VO2PZ+ndnFF5|vM8VC(%wihkmCtW z6A6*id(n*81})XyFj(!sbS7#rQWD;Gq`g{GU1=e_Aa_=?#d$6;&63kNxuwfM4%*_H zYybwJz&xK1h!h6@njip7_ARWVXt%wlBe)dR!{<`bkMQ?Q%@gm^qCmc~&D@kX7!1_K zwMJ#ZI~h6{2WHQeE*kM)dfz)RQ*dGE>VG;k_i50v@8}~OyB&ga?oD@kOIj>w_!Cg$F5huYc6OhV%O3|IYOWx`p{ch<6F! zOg3DTW>;|cumLBxV$p{uOtF<|A7HIH#qfDLU0=z z<3B=NWCP-PMFUviS0iG)~{$_N*qu**z}M!NhyUT)oy}j5$ol7BU%30NZ;s z3%8=zg2wyW2%d)unTOJ&^pu~yfQeuDJg*`1;G z69o^U@K;3xp3x{Dd{Eexa(I$utzg(8-vbD#5Z!9+Q2|Ypz!(>^<~JLhj=iiBe;wjW zEeba^UCfKezw@&t!pE{iTp_f=yGVKB)9u~+kAxLlVyrOLe9ok=?bc$bzHDslUqOTTuJDAEt@=_iwr`s+zUKy|jWHx1Tf1O9o z1|sEaT=W1q+Qb4KVC%dRTIF8wQ{cK294jg=^F}7lJFmDSozj&CfAhL?`jml`zzuUGP3)R!=nM}+#m(rkv+K>hq;uiidW*82VGuP{ z20SnOJygU?ko@8n%o32wQ1B!Z&tv2<;m}9Lh0Kx{joTXyIQ82@{Elba(zlUU!8e6z zd-m)6U4BX=5xq*ko8enoq}`^_aO>{AFM3Sk(jwjOn%un`a2zJmQZ}aBignF{kJ3bi z)NE_*#HlGx(r-*c1LE-EII!-aNAJ46H)&y-S=4C zPS9$V4^|6VF1|uKRP#9{RH-el`|UuU_EvVo|6SL~xSj#qg+{YtX$o!w0J678^e!PhGaAE zoMdSRoC~x!gjd;b(6;!s%`^sF&t()hO6cRh;Dvs*!FGQH%NXi-=e&aw=Y4;K{=v=T zNv!mNCma~3H41%|3P}bP{hMorZV=}cQZ49nN8v#zx3(zMa3M4Y zosNAev=4AEQfR;NKGJ=D+x;>P&G95^yL3?a+R6-NtH*iur~FGHcyHK>Zx%Jq5%D`* z2dVh05`IVhS>VxrLr{SILkE%O@*yb&HJrZ4$H&FA^W+#T>3|382dT)nx4l)cYqk`w zvpZ9s^KAI!oy$5ObCedlH<0`LlO8Y%6z!YUvALMS!5_5WrdU1Y$78m~#@z|CLCbun z#r{oLs0|6I0CZh<>x%`})!$Iq@kD)foSk!JZLz1Wy_VCdQwEri9Pi_vzWuS`PC|vw<(`UK>L^r;a*+P#b48Op7`6Ao@SO%{ilel+y)7;tf;|}g z^hAU5?^&k5A42)h2U7km|I2}YIq?6M1Fcl@3vjR)DlOpQQNMzKKayf{FN#I<{mc#7 zxj-N^f#z966HsKnGG567Ih3_^7oX; z&_nV+KplaD?4P!l7H#jEt!|M;L@7BO8K0rhJmV8&R zy3$PzjSS74U}VlRwJJpH~1g)V-7Dlxj?2L;a{P9+!lm0*_# zlan}=mz0z{k&p+M`;~x;l7Um?RQ}Y+WW>azzSPL1#KdI&)L<%dUl0fCZa%Ki-A8X3106i5E3fvz9#t3O5+nnLPEt8 z<3B6Ofy)UMTSN&7MOzdJfBz+z{9Yg-8ZkKuF);}_aUda>0C7)&;1{T(bVNjSqCqdf zqP##$(;9W1DU!AWaEL%a01m$iyG6Qz!vP4KU4Oa(U!MnnKybe+)SKI@%bS~! zD-axz_o>eX#^w2FN>E^YFR(D}0(1scp7@+&T$I=+<>#k4r=7k#2cP@|!IAIj-RVjX z7gv`r<0q%5XJ-MY;3EJ9;JE|eW9+lewxz}1rjz62ldYW-|6}kG6gV}q?{|cHeEucj z^u*@~`8c8Q#QzAqKMtH3Kkz@s+R%}nMO-l}t+N4Ka)>xl(B{{H-Ic=08rs#-(G|+d znbPIg0d7Muk%`-%2bqI~<%us3@)H&o4u2jnHxnQMrT}JS78YhEUuI+`W)_wJW)M&d zW?~Lt!C-zS%f-UNCHo9G00Y1Hqnb%ILxzb-CPS6!k7^K8APWYQdaf)JlWeZ~pS55n zR^T2?vhT#0nZ@48{$0%!$OvX;^JM|EF$XassDU)!X(J0VGYbZ5gSAuuGA)xqGpMtP zPo5;s2F-vc5!h~+4T7RBOj%k^Ed)Wqvu!|C-DL0_@_bj@4sx zhCo5rWYjcpEr5cAzNkrXe;j~2pk~!ggf~-`}_DfBRPfp02bB9&E3Pp%gfW#*8|Kd4Z*nq5o3Ns&bbPEG@9X&l=4ZRR^ z5P%INSwmwVBQyhD4K;Oj9RnXDka2(!lChx<&_fj+KYdhfEnQu0Enj`Go?Za(p#aoN zYX)e6)l~hpz^ZCMNfW^{2^D`;WR+KnD!#y%;w#_?tRjJ^R|rr9D=7FQJ`km!b#gHZ zV7cE*feL8ya`Flaz(?R;l>rJE^13cs^72|Py7B@4SuGo|>?|keyc{6=-_=1|u5xm& zTS0$U1J_~5xqP#cle78eBKOy?BxVV5kDNA+oAx0gh7Z6TU@}fIkaS=jvLValyji& zXN-55eyC+?nR$wadw1MB=CP~u$}C@74XeNA>C{z-%FEM_Abl+w_JC3SfuE1hvEA$r zyb0dB(%3bJAys!c=OJ`4*EcXQ;1L&L3mt$p;#r}f<53Iy0R8{B@GleoMZ&*Y@UIs9 zs|EjR!M|GYuNM5P1^;TnzgqCG7W{vv1;RS_UT(e4s(k-PzZ9rYM_}9wCno*kaDy#~ zsM(J%W-m7Nq~X)$#s~hAX_0KwQjvP5KT2vFWs_F)x-Yx$B*?%Fhke+A2eHIHgwRJJ zM^#Bl8+xtfm)dr?$|im`?*7#MsXwXf`i+$tNo+SI^iguWz(`I%{1AR6VduR;u1QRi z8xdSIT3w_`v|I8xg`7&0KIKD5$iRSuOd{L)Tdwh%yu;B#QBsk&Z_CAAej|0v)q&J| zlNd*kr|w9!c(iyq2!7aTb?EzYYHnIb(GMGXA-BJl2w$Wn3=rz9mO0dGa> z=QJ5ha60*3WpsILWV35uiKE@}erio1HOF9CyP^4t^$hN4AT9r0w?=lYPg1w1+GM&f z!7)P?l8wEIJhd(8wKcYaJLTo6rF*Nir5W87OQrE7=QV6Bf3w0sHC{!rEZvd5=^N)_ z0_-hGuw-}Q`0;w+8)FxitG&&BcBUrkt34*0iJTT$wiF>j&&Q3?bw!6z?=`ATx=p4E z(>Q0G8I>+(sK+7r%!@_GOLc5DBW6t<6fW^(F9T!e+{CO$T7y>5gfx z(Ej*OMv;9d{QU`p5ri0jrU;1TT2Xugk6tDI4juInB*So0`>|mO9$Vc@*_pVR=CNvP zJF8)7F*eiXxr8pxh$?F`9GM*Y(TXx#I|(~e?iRIYyvfP3C3vTI-K(vWE*M6+D#V>Y zx3@6lhVEWwLft;c9P63mMs2Gp4qj%mQ|8AQY{pb8f~gcPv+PMi%=|+<0$R<+)v1mZ z7d%FzqU9G;zOAKt+>q$l9Pgery~#{5V9&4r%4a}ysHeEJx&lXY0w?Sojp=b#9)+0D zSAX6!X**b5?H(*YkXOnvq8Bh~iR({_BTtas9!~|S1=+RW7n_X^y4>^HpCp|U1?sM&%as^Y zZ+9E=y(!%LD=C_%O7{CTEBJ&cD1uq(`8!ai%PtpGG#F60`ASP0NEYj_9XBIANaXI4Id3UfD&;)vc7Xln zmE@^kAsffK88?N-MZxcg=~>st1ws7UKt)zrKl9c?hKh*x{($6=F{f{{fWJoieVSrU`%38}jkBT@+x zoczY&uz5jOmd~5`y)1FNDHNs9&ssHa)QD|>CuY!tjhF3G_=D%CT)Ic}9`Mg#lKpp9 zEAI*k*Es1J=amw@v&r(6Vdv*7qvhT#3HtSbk6+Hx;Ga9Dj^7PuF>&YB7J0N-xjFF( zJSF2QxUN4u>xEvyDh|}bt$51kNc4^=C*2olk=Fj!6j7l2*5V z*!-{=YyS2iQOj!D;D||8Gyfb{EW%hw(=e>mv;1|3x#gEnQ#=(SYg;O+7;9(@a+;w|ed;n5IUanq1b?@zKXIe>j&vTdvOTYxKdY?yXX8j%azw z9oWIJgh2%CD8Ld1;;oq^TQcQvxZ<(% z=q_`7`KzCbqTh`f4=OckO8Fhg%CsLJ8jfmgmW=MsNu$NcFeNlE2GRM4Xd2fSHQ2aG zp>MiR`QEOVOW&rH-Ci!X!WNs#rC~GwNPJ4GQ){sBD=v%=3Sp5L(_>zHOqL#!Fg;Qx z=de<*7jg4k`{+EM(9jPXG%emgok^*&b=^}rlZ*dooBAF}`kaH80)e-uc3?sf=>-Hd z_mh_$k3T-NZMyi8BtfO3J!?HO9qfEQVNn?4rC8}~GS$!BJDbeQR?%aUM5kvm2(dfr z9~~?&8w_D$9r%&=6HEWu%+ePq0|Tk6-^&c!_R0zG*tnmJA5Ji(%6Xq&Zf^qHm8p?m zZxynUU!V7IP^MtD~O<(AX@)iaK(>y_jQy^;-u1C#UJ)wuN?m0Q;fl0iY|Wucb2b?2)__^nvC z*wmBIXHTd*J7sgveu{M=7|}2qnk$6ANyo=}2){}uhgzH(uJ1M+_H?Mlc;G#{St3dB zeb4gr-!*3Dp%YQVVP;v3e9nGXGVSL%R= zu6vcXse(7@YyELEC87;EEz?djkxCpB0T>Oi$X%l1m(zRY(oW)T6Z`Guy(5U5{I>;C z??K`TaBka^2z}{Ab&6?ud$YHy7Yo&V+d(MbII%l!t#{Kgl%YkZMaM?Jx_Y6S_*-do zN$e;gU^H6^h}psV82j+rx9wHUDHMrJ~DeL*a5}jl@WEr&4Knrn_EpG{sr& z6g`*z#(DP1jg}das@0U8dF2T2Uw-^Ap?UeWt{aFIMm<(iKknzBDxpnu69jp)`^sSgIo@qEdQ6wycKFT$(B@Y?m9 z2)^|dxfCfj^HUEKn-;HxX2~m~>E^4PtAII?ntpDqIZ;K1ek0=3yFG|r{WpKc*nC$S z**TM+TB^DTXj8O3b@|)AC z#oLiLVv>@A8_|yl{?Qud=e{c|?6e+&_9mBIZ^YcTmE`;RKQEaYd~Z4nw>&&Zp@JUt zaWKaGzWX(M4$~YDrxgJ((Jr4$Z_yeOvM&HOrJ(4aE8xZ{;*5FYO38T(qEVs2Z-*6{W zoJyKN_FHF_)>GK`+WGfjmA0GBEi>y)X9cIz?LGyRKkZ~`C8`WaeOj&c0>PjKccd8G zQ*Y1iFY_&RWm+{R^wGC0U)&aa#RQ`c`xLv#qr8Y!G0;kCmw@*_(*HMZ5xnuGquGCr zZrh@BtI5}})Zo6C(zF-q%p}yfp1PV-bK7ssCX=XK1%7nrPxYar16+J$q!T4VB*Uz`Fo<<>M8kvmVVkN&n%mnd&iV&@u6-klr%Y&EoQ<`eTo(>q992_q6}D zBnZf-2#>Hfl81Y&ryM4Ycn4(JG&y!2KVdP^b~u~sO{=~(pRnR{cN)h@_)TG4Gz3A> zs{qgvh|16b;&%IjzCFIT@fSPA_Hsh@<0@+X$BY(A^?Mlf_dKI`IX&#!@i!ea#MHB0 zkJRNJ{;zfb5THgazN;79&~ovp>FG|amuoLe#a=S+m!%{!TbSL8!V|Xb!<`zHD=QIU z72L-NwyUcH&WKo5#`Sj@v|c0M5}Nk&FC_zS=+;?F>yBUf#5pMQLq1iCd&X^hk77k0 z@x%X7g)miWUbeow5(9}U!v#whl}pW6JwFm^bX~hwr z=N{mn6&N1;4DPDXl{pu@w;Cx1c`ry>7&L|KU$6OmV3Whe;t!&~I?m4~X!r+3pbhVk z(t;1!8!{ivJ!QeLg`7^#s`MtsHMuVara;+JTrXA@+YHMJh6DZtJ05`bcLWa*)Rk!w zQhwt!m#^cIC!Ro^ZTeLdTJ znp=m~(|9Po(z3&D(V0YV*2}5;LyPA@KTl!FK!DGd&I*9g^aG4|(9n5t9VKAT(usY0 zz;LV)tP#A(m1w3xz0>5XvnhH`?)%YG$Ja-rM{SoGq9wXj%CAWf4gFt6e~?Po7ZG;d zJA}lgdCa>~bj8}7UPa7%)?ALp<0&+K&<3t~>VXI^Zn=Jl0taIcfSyue$zJb8-X=6B zjJB!7=@3_AYSEEcmp-3m`-K8gm)=9QBSaeLFUBhs*Gz7{*vMKX>$lY7K1{afyUpru zXTqi8Ex(LS_=}zZg5EsWIPFh*>~J4=EhQ&}_b5u1`|(ssw|NH}eAl^qvl|-8Ry|!1 zD+T+f+XEO=j4nXB7DU+7!&t8@fL}l7I1w7RcAN8MJ&>gMjq#Cu@wpLfax2MPMEHc& z4lq{=a6q1U@CgN6fJ+J=5WqDIPprN*-!7@v9F6U7F*}?zj-#WeTaGezh6*whax9Kq z^iI60`+X?&Kh|^XiLcQ%J4>UGr(4G(qQU_Ak|rluO;mB(b#O1+o?z){v-h-VXXsXv z<=^yySR~JTLgoXaF$ntSRHtO{p#$6+v*FbgCp0$ujr*n#uZ)|qsBSMO7DI=g&JKT; zWQmy)`ybi}Ap`OC%Wy2X-tjUQd`K6hQ*-cic;%Z$3UV2a@Oa+5H83!?ZFT?jO>T!#j1wH z;I-AjUW8!vwsp~b-DOGyq_|0n5Xs`K48h|MdH|0*Y+-i+*{GxYUZCAd%--%4)9?Ps zmd>@YF0pC4U5%qpdMNm9)*nanofHw>Kb-*J(;^67%3%?24%-2x8Yjrh(bZ&=dhi76hKwvFN)$rwyjKoQg`2 ze-hAq`h@o+gh}MlF?w*dwU)H8(?0@ygt^i`z!V@Dz^0vdvs_!4N{clP=d-BuWx1ov zyMJ?bL|FW1@*1ijgYT2H3FDUaieOW1Oyqu}^^v zBGgeIL3Gdj?K-V+`X@|;B!gc@Vk8ZkdpiYFI5F5LU7xM^%-G8B;dqMlvg(jNj<)${ z^ask(Lm3sP2^z1QW@&;Q+`pW>1B@aUBQz#a6_oe5g%Zh(18C^hI|O`I zM9l7hk+4Pj9X+M%mr7ad9B8D4@Ndc#)R_~WMFRm(&mRrP@9w7 z5>lq{RW)jLSZwMI2voFfMApTf)a_I^3dh+tMZ{w~l*4o{21KZg1B@-FGqEjo@boJxpD{yTkwWGAN|U!V?PjZn@qpOzL!M z3to*FhHnbKK?K#u4SR<-%;e{6GCa?)DF8#>`kLfB^C{;uFwkMrK)?#Z-A!xoI1=8= z$E3J84Mxe^#6~yAYOHpUU|;iuc;oR&{z5^7<@9A3aFdR=CsuzHHzf^_ax*tG#B@FO zC?*|!@&FO%_y!V{BDH=Cc`Of<8Ib~cG9@{fZv=oSZLZ!uf=DV+$Np+(v51{MVZT>T zl~)E>zxNBIgefkJu+tqUanjG_EM1rGX_!_)>sEq##dV!^QZsXiKo&i=p+1)~0F7yc zcbV(b1@3YW{T|MF*PJAWQ+r>N{FM`6?HcEeT1M!pJcg2PhsA#phXFctf<(k3+Qq(F z;!WY)8V~Ig+IUBM{X+Oe4&l5?(~-Q8^q*%(BjAWNCa@1NIO5&$7CiVIm3Y_6PHXd1 zNwdX5%O|+fu;(xOfkLcK%_%5@ZVl{ivt6LW&+WOJ1}DFXxUVC>)AB>}0uwW>x6>gM z%YzgZ;VR5fzJP|zSgXPbsP~sqFl`@3t#llB91m4iI;2!_duJcTLv9{7MjrRi4z<7| z3ZaU>JBI&dAs%XE*thuojpI$@mL$%?>+ahd{+$_*aVAwySlRV$q8G5G{U1?4&X^60)=3ZH9=V`2=~j>^dCL6>N7si>XP+3tr5t}R58iHG+LhBqt_Jsyk;CFv`MP? zdfzt9PHMQmql9SAX+CW!RqykJS2nbVczgk0*l zw12Ul(Q&svXXoX$a=AWG>6QGYd+-s~A$;i;_++tob99|}@YZTR^yYM1Ag3c%P1gN# zvtZ|^ZBb)k0b&Q=OJ)E?p3QF4asZ0Z0WE>IU&?=J>D7`=x`mnY-&*!d?A9~YJ?joI z+G`?7e0?(^)a@Vpe>MJ6h{^FM>rL&d5JAm`Y00^)mi2V_P!a{>>Nu&0%~5 z;U@AT5c&45!km5}Vc{d(j|^|S^m2O*hgRFRcY}9h`ov z!^!eUI9=5jXiLwr#OCC2v_l{fpW|Ms=jeEQqiM6{mm{ind|dc_}pG=Jeat^yeBN?^?G z-JBG&DKAeB-TM5Tsc7tN4GtR5-15abV6Qj4(Sco$e$ho=IEIlBL*l1OfrEBmdBK|X z1n;`-;%%o|n_vp}*yt53Wg&@MC>RcxkiAfmMnEB*6*1x?DGA}mr!K_<4ESpV(d6?n zNq<#6&jkB8-lhv1wwC1Cm{}rKJ=fL1JZ;5aaTXoV41|=BMu!}=h(_tsI=Spt%(cPh zePMsO?$BFV`4n2!v)-f9psD0F{_3Yhr2$Ku&Z>iL4QLAupe>Dzqgmo|iO&{J=9^}0 zMz#hVNv6uog0P*E9XG8)@OWknH#>|vEH+~ zaWXF?(PiJK3VZSEex5fAo4A#W9&P~vjl;02_wM0*t1m9(_;tN=%UXYn-lKq?^}&nl zzo3hS1Oy*gYR}047>lLnkQKzIJxs$+nsjL0CbJqcu}O-Pn&@vnfO1;>#%eKB9V@l- zt8o}Y+Hk^`@YL|Tak!M%gWGAm3vD&0v-T6tPM+)C*rtQGdcJUY)*DoxKPjt|B4R6S z$MuK8`e2kLkhj4wf^_aO6(60BWzmm`%IzVeqxDp*;2@T^oShjGN}As%b^fWY#4plc z+jaaPl1+12>ZUwvx?U5zf{j8p(;AceQnXGc|3g*PBM~lC(t;ldntrkv>qF=D!;6(O z!kUY5{KDdClO`wg&Bo{ccdtn_5q9$zIBt!=|IQrcO2g?sq2qk9<6CGmyW$H>^)A*^ zIKebty{alcZ{%Ae&M|C8I5j#pg~}EA%}|7$kOLU}IX+Ei%lixi&bO#1=*E1cpA?xR z(RBXWg^BmU1u)AtKK>};7rKKm7}x5)Mp-NEp9i-JOL(1ca8d@e-gZ3h4YL(m-=R-+ zxk{;l-sk6TKA=j|>#Df-Cw+Q-R3AFi9T^~j!K5&Xt=K0Yc?N|a_AZBS&o`ZTSyNfP z?f6Ku7-i(+3%7V;{Hwt|5U@SC_Z_BJsl&0K?$657;x(;mIqR|<3rt0!C-@yyoeS`% z*0NI^i2O~(3xt2G5&l1PU3EZ{-P;}$lu$qvP>~P`l@F(H6LKvOW-OXsm_MPGT{l3@l_r8CKb2^{roafxvecjh}pR>ITEOojiqTdXo zYUM%DLdmRF;r1rpL)MpIVL{WxAmH^tzBsSsk0-SPAcuZI@sJuf>aaH|I=tKipgUm6 zMW>QFSlWCH@r^Y6pVuz3jz@h{aeX-IokfE5OlVX>Y{d^n?&&s$1p5Xjg1W>fqEnqW zM^rsGQ^8dL;zrwfg-m_9#eHhQ-Oyn@Ct~I1!q9(tioB1J zoxw_yjBvLetz=ZR=vFt0=W;mATcr(UI!U?H;PnxF&|ej;2H}-Z7CFtPo+&#XsB&MY z-hrlN-bb$#hR?T*`TQX25J>3%itIof)@AD#J76DdDl}IacHWRItXwUdREluMO>aq7 z?_1UA!oat_zdnd?w1|y|Adg=HtK_)Dd6yTmh750y;qn8qX$`4`oh0++vln)Q<&Olg z|6=^)-6xQnNqJtoS-OSVW|$(|{Y(Yc!#T!LDbJhmcZ}01IK()`-rUhPaM&VbN!#?N ztyZqS*n9!9#}|&##NCTh^(npgxj{B20hes|70Ags{ipyWEM9CHynk&pVS9C;*t+si zVrcO5RDMI}?DzNMVZiROk^IYS{O`HtH9|I*N8zJjw#7a9ciILJKRR>_!(sYfc1Gt+ z^-X;4`-5A1^c?wQoW_9^Om2;U`tv*25&Ijb-3)35Gh1RLb(n8Fx#wt8V4zXb z$TgZ<_uF&&Z&XW6qHONB+D2h?B=Eb__+hI<*hR#k?VeBS(=DNEwTD7{02s#nKn(0Y zzRU@*Ov@NgLhxD#m_4~2>D2hN#jd(|Zo65xk>c>6PcJU+UuH5fiDKwH*Q;erQ>qcn zrBeKu>%i0L+yTV1!xIA4VXNjB=L7;yUz%wsB4TGW8IDx8MRXe=Nv7<@vEbJx71x7? z-S;z51Nf=XQ#ggo#8~-x#kA&OP0 z!I8MCXBa{8>vKjNsGir za9CMgI~D&=H!19>L*@6`^}siB3KX=DNTH4FU+>TZZPya4{lL90YpJUGAD7P3SLgLf zN~>9mJTvZ?`dl1h>N;F}t8nh(Kh7kuSkj?qZtLE>tYWbuJ{eT7;<-9}I9bAd=&(JC z|CB4Lk{P`I#Q|}&8p*#=Lh$s!khn}Xao6|0`OiH<^!2eO;kFxVgv(c%|4By#GZ?6u zXnKW0hfA^WOmrz<4PkrGZ$u0y)^O7BX^ND*7d8CcWv%Amr^H9i1=$q?>^Beu7PXM! zd7yH&d}~9fBo#nI{L455%?hStB|W+0tM|?6_B<4z&hVy-KyQMtm-LQjvfW-+w8}e z$6rr=XL(%ix@?u%BlAz|ka*ohS7dY5lfyiXQaf)W+#Y`Tc7cCicka9p>YD(uysx7VrvQubxv9FQgknjBb&RIA?(%^353=!S^?_>7?4E&tTZ`np zVX}&;Q}$8ztrCl&34!iAzt+9?r)XMnZU(t)dxy8RvIT*xWB}yC<3322VDxvYvm-Za zokP6vSVf+MA=5T#|Aw%OAIjIz#~5qu6#g5P{(1*?B~RxOm(3%Cs^(JWTq}HV<;p(B zG&r}UK_OdzHaOGE4lcrM9e7}>2hybw(aMO-DXF3Rstx868JW;nRkO0j9XvNwZ0#S< z*TyR|ml~6iEzk8YOOP$klFWz(p=tyu{__6n+Cg|PYN^0$dVSA3U_IIQ!~8gr z>4rn_=~IfJBsev1wLpa=qq~Aj44ywH95u^b?X*DhqteyT`e$^E^T=NHPDplW_4jz& z0bcbj%6~4QLG<`*8IQza<+Y2dv90_lyWYhCY_KFhqTqWtWFm$jRL@0!#2IDp(B?s9 z@07jYmo-KbnK|#pE^LFy%92P@S4|(>ehj8Z1haqQ#)&KBXIJ%xq@C^(@T>`f+uf6* zL*_AF7|Mj5hV6P>bSJTTO}qq5>~`9m&oUs3jQPp@n#A8X@BXS7s=Cyj3tvyFb2Bdi zMvh))@xM_h+D8&l<~xdI5{jS7t9~%5^wq3XP<2{&@^vxrZ3Mn+J2W+rP;u6Vl71rk z0Ll%+x)j|c$lY%V9Bv5gFM7}TkrLa?4!*Xlhbv7yi?Wk9{#hahgWM%SKGA-D<>_GI z>;r3-t^SJ2I49Mb=hIjq@8owr;!g#P_reR=PFjg18bGkQ)px%!cL|x}B}uL*)^Em% zZ0V>CIV!K4*0o$0U~zrqQr!5jt8U>d`V{DysLEQx9D~7~nk>8z?1~05LRW6u{>t;R z?z+Lsb1kDpW~SlM5t-W3Of2&e)z1U~rOQUW)<7qc5;`?n>E*KZKn&;{btr+RC%v%E zcG?2g4T1W}e<>FMJQyPVH-u7_gW`q(a+f5Y!@Fhyt5r%OC&!$yx@(4U&@rT!c-=W< zPbM-{vv~erqS|s78nqq89m2Q2={}xo@8dG@=3q5p)xir7*9_qQol?3LNRIhzt!F$H zMp}_k!XiQJl}#bIuyuT)5z^}&mxC;8Ec|nojH=h`tujOZ6&AadkwsUeB@FF`${Myk z=XO}_Dz^t3PKkfPesJMZYjxwwfsApJEKJw+SVCcJfC{rh3SxlB^7a@$e_AMxL(JB5 zM{KaBFIA<`r^SsIkHcY2DgK3f0ARQYRdjc>-qdSnc}{~#EG4SNgMA8%MAb-zwN?mqePzI{tt4}&YV5`CHxwkC=k|Xywcqb_Ya&x= z#3Xae2#Va*8*Qa1@u`v?ZH!y8<51$h^O6lOV;6vrty=);_n(CqeUvSRzGkfwl1AWW zlUYR;QQoT~Da+CKdGUK43Of36_y09}J*F_AtMEIfr0f|qUtD)R0*Bw(Yvr3$f}HZd zZBKrKfoS7m!h74~In-~Gu!YXER0xdjCo2n_I(QS5Wbfc``FlIijQ$ZW+ULmLiwWS_ zuoz}H+%uexX3+kDw*K@jt7vlChV$(Dk0oXR$L*E_mspt4>fnMFQ=au{uXldj{A2_^ zjR^%e)Nj82vbxu5`JeSIB0DRRJy>CGvv#PfV{~I`x0NLqQGvWAbr`xt4Bds7vKp)x z(gR7!z2IySz|4ZkF^kyQz-Y{rvFxL7laE@7$o+r?a30{3kI0sMb7!>%J5} zELO`1`099lyWyBp1a7_RuAE#?Wa^%CKu6~9Dy>N%x z$H|1}&2g1WCS$M*{|2kRvS%Ne`)^A}dp%#TtE{dV%*0q4Fd0wf0gt)ii(=9o0;f8yc#Wm8h#GzLqZrRvxT8N&9j$sdb$+KEJN}$#j1Ez<%Sk6mr zq8hgL(iy+mGX5>E?*}l(?S2aMYyO*oc@&fx{I->MNVK&Q_rw$eIRx`KBG1%l}sE>oIv`0o*`HFZW(P(2xzQ4Yu^ zzL&+J1B6xrgqN>P>Wd*HX656w?f&;HfXGZ>2<^S+%i%W~FPk(`{8wCK%gcjuN{TGU zJsqsvS&JAovYTkLtF)c^@-dW4+R&)Rc&5VPqt z8iBtkCkGFXD#j?DsM)u2c+g=QMZK84GugI$@Ap6xb10kRBrQ~PY(u%#;w0>i$Va`! z+#OdKcV=361lsBf3d)_F99&&9zkXe+YmO>3pWc}b${0#jiILbkn7+0%MX(APc64J+ zN!dHMQ)<%>y7p0DuOQSy8tuxR{TcZ5>np&7X<$`5<``yg>w^<4KTB`OJh&2&&sxrXx}2}xtBG)iE$<20`-r9a z*gy!9mEouKbpoEDD}4Q>o5GH?)m)Gyo`5DCJB~-?PTL=iGOvhqLiTX3!?a=M%@C^9 z&dlsm!1+)$wdflTCM%2UVOKTNW@cKzAKf>0y1+a{hbgPYOPMLjz*sHJN)0M%>>R$! z#)#Su&Tx?yfq_wwV0c4=!{>4Ml77BYlwqaWr^ax&XeGS`s+hObpf9J~xi81CVybhw zP@muS|ML%qYS~Jc!QA1^nl80A!rAp7G{H>Q?e0yZeWstw;hSF{!#f}z<3HP{U7(KzJB}uD=wF=LVs<+FTdujV0`zpG~-l?YHpn;}tKRTdMJ%{P-6RtaXvq z2gV3q67wbWTO`X*n_NV*N_#X%dP%UN@Di!mW4JQ~gD?2s?sVy=zbVuB=Gf0CcFA~e z8Y`qq`_`d+y29lEkB^tuF)IJU@oHisB;+A@=7j z5~e#@%}WhpZ#r~-*%c{y@S-PFwGBL<#pueQ^2H02G$N>*m9I2bRx+GiG1+W+j;ZSA8gpNl*4-l1QKun-Ay#ZEy})4dTp*_FJ+gfK;P84b2fpiy}~6^ySy@AOUKYGeCV#Pmdxz_3ejUoP=_2u64D}SpErT>J6mzuf@l8p6;*wdi;YMXPz7gDl@f&gH=`;snMog zN;$6?I@6^)z#0`IG67yyzyui5awv7?u-+h35I*NyM}`hDpzMFBv-3;Mwa@^k%aLdPRDR& zW=^yF7y6$On7bq5UFOPJtzNsY#)Y-vhheMKd${sh?)yJW3qXF(r*RmA z)klKvzr34(1U$N!vA{u;<>rEg82B&lwmsS2o%qkC_?P%U^!_!_Pv3Gnk{# zb>VCvU??>%D892m*`{6emv7lC{Z@A)D zavEexhZpFI#hhwayBFv-L`@w-OZ4kJ@F?v1tK7*A3-lj!gjl|F`jeXh-7pm;=F&ZM znZGKp!IinM%sq}Lw>cu~DOA>L=8S39Uvdk>Yfv5f-+sVM%5X3785)*q${Pz&f4F(8 zFgHiWu@-;ehWp(0u7%)yuuSEA;Wre3e)+fd3j}aHtl!M>?MVp1I(NFOt5APj0L?ef zCBcmJ5NOXHbi_>e9-r&72vu?9A;aNjW9;K&*z)F6_lgo2zV1XkI`MZttS&&>xz#B; zA4Awv8!hauny4wU&KB92`pB*3A+fWdVCS#qOKIw3PyV0zze2Pk!lD=0f6qUVMM0ZT zs{ecW)ti$#+;+R0U3Ccl8mYqhnV5#ZqC4*fseIu6QsGcvxkdbWshc}ogt2dJFqo4$ zd4ff0K|x9fJ8-CZ%mTHDK&^Qj=M4BDfHUCZf@mHaX(Y;m0D>gOAA`Tx5Q{FRJ1 z8Q`0dqRzq6qPQo;9S9%OpC7!csVeA*le-}C)lb$1(4B{A?7?W0({F=~s7wJ;<$s;a zJ#q4Ul^cphdOx;YE`dJ+wPQ4QG5Qs(ev(0X^5Jhk70*pOI)|7~(J^W~wFz6V1+DUt zAN30e$-xs|D+Ky&&|>GJ!A6aTndg| z__M3fMbCI;&nZ5(Z#lm~yR3LifZ#lnTOMkOQIZI_eFP7&Z&MtZq+aykZeb zcNTMH$I;&R8x(Scm=Qh2UoE>|pMgo(_4y0fIoM#rm9-_dy7vd&_>T7HRPYLlR!+~D zq1L`*1ZGF3SfzH$XC03n0!%%~LE~)c3YPcz1M^aPZOehpaoYrCS4p?swPvA4r$&^dn;q>* ziK9kDWffx&S4>Nx4>75=fQjG!HA;wQnj;8So}YtTMby2c=)Q{V?dNmTshA8Jv`^5# zG!}sxG7TQ8_3hDt+ZK0rO7uJoy!7{a2`RxL$Qgy}T%vCiwX1G`ZYN#F*C`U6l0G_2$@;1|aT;r4i%O1JK>KjH_xclc3rz1cm& zJHw2@zl$>Tck&!Q}ukUJ$UD!%wf@@LQ8PX2Q*x9D%EV{v zvR=2f&bb2wyh234v+oz!)UyEs=3cE=NOge9I;!qL?$1K=8u0 zIKj@h=Z+eA7TcV^ogl+xAA6uk^5^Em8>-0hOrK|%(mSR7=8Ne}u{d-LVPD7S5;d;j zQ5jSg%x=r?o)P26#9KbcXGXyrhNd}%(@}+#ZI)$Xk-~Fx*sF8wCDQ^ z0XeJQ!A}b~oq@WJdPf)n=(~ZjThA$_7K3;F9q_<57Ru(Ic4omN&YHn2%LBtwGtI)- zo;twLCP{tIO)5@?X8-1D!KVdD#u~ngzmd&`0`y~eKfgOyD9FG_IB3ruk;aXHUNWPR zbRB<0-RRGE4ManXt^F72wF3Q|Ho|U$b#4pDkls7eYqEA|#9X`BNThUS{?j=DAdEan zGy_n#RsRkGIyrw{%GUSGTPlc?sN2e2Fhvzb{lU317{5NhHq7a5HB`2oJ3JTT$+f+( zwYnr~))Xtg*Gbe9j^SVD?>qhElr{cFoMl1o6ms5IvKQ9`0sxS#cUZX26Wr2DWoib` zsD-)mN2wRhGU)TxHl8K+;UbT-scVb6*{xW&cB#>v5AjSUfXcYSdZ36_rVSJarxIUQ zfO;>P@d`ix)y83FVB@+DPHP{Z8zp?WqgPCsWYNYV?Z|WD*B?`y_?SFk$gx&@RJv%; z&KqVlyWm-m+BK=s!Ol#ZdSOw5^%Rhf#NWyU=%09gi}LCn;GUD$k`TTZyxt>fUN+8) zVYbxsL*<**A?KrfVr&^C{60|gOC33@q!*&mQ%j!?qOoHygQY?|3t{~81VdMUIO2j{ z!j>P9Mfe2mjdXQa)?3b~No{opoDX-X-s&|J+xejbxbs{R$S?Xf9i{e#=9f77vmN~B zrTd)RHjIwf~!fX9Y)!0(%bobMweqCSLt!@i~haLuV^Dg?uSuu@4| zGVB^*SRX(!o*DzT+$?m{p2UfT@no|Cs+on(dkp-1H#>6fxEsdk+IN?hN=I*<2xMjb z{^629)rSL$up5ujt0vR=gp$OQ}-SCXn9*<$RFUu!I=W zD#kX8Ry~dXWBuO=QlcJGpqFLqTlehtU~b#4h-jH7NUYOT;SoB44t&k$sJM}gshQH@ zKog`S${`STMdNX@317tcM$-~Qz}K~Vzjw#Mq3fsc*16UA z{i~X{fB763x`J+$SCXqZ4E!XnFV{M~7>Id%$d8v50sI>a*I@aVXtw`xZ|lCl%(E*i@o1A}a#bOjC^tiHYzU^`?=& z;q9?TAx{#FgO>fihuT(*dURO5Tjs1&sRJ`j8FENzM>m7jdQBwa^M1!$2C;ch zg5Dj_PB!YzhVMWeiS5u9UrP|sHNeTOt{TDY0{vL+HVnG9{7XjcDj&T(>T`9`G&Xm| zc(?3g;475^s4plILWO%u(R&bU{CVs3Om@G%D3YheI!?T^9e9;D6J6~&%SHYtDj3g` zpBz0L{=qd;KI}SJ)iL|+ggG^Hw0&Kb``$Tj=uwTVKZ!;J9Qib&Brp>7m>_RbpD>U} z1QA24K|k6aEw3@~MSFElEq)e_vBxK8xX}^=m)8|}qjFl6E+TTosm*(*g-_fY+X6V4 zONQ7X;{n+-DEK$Hdq}a7=Zo1x2OKAuyHSMQ^-Hl1_5BvJJ`%<@uKvN{K2Lh6|IjCp zte5f(cId^P3i&iJWBuwKLU*BN_ZjP19oYSV%?u;qf#8C_vx2=p5t z;v~jXt6cGJ-R&`URvoB1d!^NW1mRkb67dRXI>vKicDUUkrldY2&%-Uv)K_aDa%3c} zwaLLyIY-#IIl%ruqAD^pTz4H)^fc7pwm^S{KiPKB%cct)dSOtZ7~0u6gf|ED{qs3u8Kaa zeSd|#=Ebni>7{+j7jLgX-MRP+G93@KHp$Uym4CnDf=~A56ZokN&JjT)d%^U zr4{d48ZOV$Qb}yo);$|qkZtw1KAzA#a{PStXK9*wg%^{_AV3gT5Oxp z2Ir3nr!LELaIr{z?Oq#CmRIz?%ZSCYZ)O|Q*fD;6lz*FSPt+J1(Brax5T5ZM*~4Ye zl`RJtHuAVGn6Q+7LL6#+>gvkAOD6WK-R~#W4O@>9cLxV0X>4<-T73hL77DLx zQCBZtV_5XR6Y_g%p82EWb=-nwhL-v*m`-K7?5tJdv8VYIsJZF!Fg*g!a^qg=_N-BRD>LRF}!po%A&sZidL@CaD_%f zjz9gc(j1PQB!6Dvl;@TTjm_;KNwOjyy@N`+-mA793}SYy7*jBu(q>^2vm4+j)*UIF z-5G6}f7xF0AzM-xo#|ghC>gXXPjP;l;<3FnVn_#VZa%NN!ep!P$H>{QK64)b=rOO; z8tqcgJdM~}_#Q31vEP}RM7y0-^p)x zahpy#I#N;Ffht{9VVhrh4yJn1y!HbenDghOqGDAAJv#Jt(lyEpyfRmZ58wo)d+H0F z%KF9E9iG<_ecSsp^^eVeFK7tj2w) z@0VR=AqvP$gu793u_%Gv7d=XL53*Z%LYaK(GVSw)QQjCdDqUr4v|C6Bwl!M1laNv$ z=wFGtt2jDrJ1|vvCjK^YlCQi$50ztQF>P;jjPi3f#oj^cesKu6B%;&PdcR&$80S*C zAol5MbsG$&c!pvVQ_x*(o#60>6enGAGtcvecr8@Heny^6RL{LLyWrM{E1d)K%Te%Ym@V0+h{{=T|Ixk_3}+vD zR=3zy-pK5yPZiO0iT! zIh{X_06lNAe#7T_@oDiKe_XXY_7k7>3*Yc_Z112U=kjd&H>N`bWuYObopcWSEtEg~ zbV$A9Ct~fos=d(2&FWobAf94ZF~7g49L}Eq(=b`Z{{Ns40Ml7$TJ^J;㱉#sl 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 RDgjd<_&D_I%U}P_{U4kf>vaGC literal 164978 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
    `!zioD`*0#QZ)~UtbrKwHc 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 + + + diff --git a/gui/res/resources.qrc b/gui/res/resources.qrc index 86efda9..b89bee7 100644 --- a/gui/res/resources.qrc +++ b/gui/res/resources.qrc @@ -5,6 +5,7 @@ discover-24px.svg discover-off-24px.svg chiaki.svg + chiaki_macos.svg console-ps4.svg console-ps5.svg diff --git a/gui/src/main.cpp b/gui/src/main.cpp index a96f007..e6597cf 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -71,7 +71,11 @@ int real_main(int argc, char *argv[]) QApplication app(argc, argv); +#ifdef Q_OS_MACOS + QApplication::setWindowIcon(QIcon(":/icons/chiaki_macos.svg")); +#else QApplication::setWindowIcon(QIcon(":/icons/chiaki.svg")); +#endif QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); From 6bfbcfc456207803a0f648eef1e6d291c868aa31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 25 Sep 2022 14:24:43 +0200 Subject: [PATCH 084/104] Update chiaki-build-switch image in build script --- scripts/switch/run-podman-build-chiaki.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/switch/run-podman-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh index 520188b..f9e05c0 100755 --- a/scripts/switch/run-podman-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -2,10 +2,10 @@ cd "`dirname $(readlink -f ${0})`/../.." -podman run \ +podman run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ - -t \ - thestr4ng3r/chiaki-build-switch:35829cc \ - -c "scripts/switch/build.sh" + -it \ + thestr4ng3r/chiaki-build-switch:v2 \ + /bin/bash -c "scripts/switch/build.sh" From 40a9dee4edc27bbfc03813ed3d410bbed44edc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 23 Oct 2022 13:42:12 +0200 Subject: [PATCH 085/104] Fix some EINTR handling --- README.md | 2 +- lib/src/http.c | 10 +++++++++- lib/src/stoppipe.c | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f306470..5be248c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/c81ogebvsmo43dd3?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) Chiaki is a Free and Open Source Software Client for PlayStation 4 and PlayStation 5 Remote Play -for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. +for Linux, FreeBSD, OpenBSD, NetBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. ![Screenshot](assets/screenshot.png) diff --git a/lib/src/http.c b/lib/src/http.c index f55c438..5c07802 100644 --- a/lib/src/http.c +++ b/lib/src/http.c @@ -146,7 +146,15 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_recv_http_header(int sock, char *buf, size_ return err; } - int received = (int)recv(sock, buf, (int)buf_size, 0); + int received; + do + { + received = (int)recv(sock, buf, (int)buf_size, 0); +#if _WIN32 + } while(false); +#else + } while(received < 0 && errno == EINTR); +#endif if(received <= 0) return received == 0 ? CHIAKI_ERR_DISCONNECTED : CHIAKI_ERR_NETWORK; diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index 7ad2d2a..003ad5d 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -147,7 +147,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_select_single(ChiakiStopPipe *sto timeout = &timeout_s; } - int r = select(nfds, &rfds, write ? &wfds : NULL, NULL, timeout); + int r; + do + { + r = select(nfds, &rfds, write ? &wfds : NULL, NULL, timeout); + } while(r < 0 && errno == EINTR); if(r < 0) return CHIAKI_ERR_UNKNOWN; From 74d39e6314b8ff8ad7dbfd55fc44cae5a383aa25 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Tue, 1 Nov 2022 10:01:21 +0100 Subject: [PATCH 086/104] lib: Add support for trigger effects and controller haptics By default, no trigger effects and haptics are requested from the console, lib users have to explicitly enable them for a session by setting the new `enable_dualsense` flag on the session's `ChiakiConnectInfo` struct. Trigger Effects are simply a new Takion message type `11` and include the type of each effect and the effect data (10 bytes) for each of the triggers. They are exposed as a new Chiaki event type `CHIAKI_EVENT_TRIGGER_EFFECTS`. Haptic effects are implemented in the protocol as a separate audio stream, for which packets are only sent when there are actually effects being played, i.e. silence is not explicitly encoded. Audio data is 3kHz little endian 16 bit stereo sent in frames of 10 samples every 100ms. Note that the Takion AV header has the codec field set to Opus, however this is not true. Users can provide a new `ChiakiAudioSink` dedicated to haptics via the new `chiaki_session_set_haptics_sink` API, which behaves identical to the regular audio sink, except that it has a lower frequency. --- lib/include/chiaki/feedback.h | 2 +- lib/include/chiaki/session.h | 21 +++++++ lib/include/chiaki/streamconnection.h | 1 + lib/include/chiaki/takion.h | 9 ++- lib/protobuf/takion.proto | 3 +- lib/src/audioreceiver.c | 10 ++-- lib/src/ctrl.c | 30 ++++++---- lib/src/feedback.c | 4 +- lib/src/session.c | 1 + lib/src/streamconnection.c | 83 ++++++++++++++++++++++++++- lib/src/takion.c | 9 ++- 11 files changed, 145 insertions(+), 28 deletions(-) diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index be66384..d0be7a1 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -38,7 +38,7 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS /** * @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12 */ -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state); +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense); #define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x5 diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 3a60417..4f9f932 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -78,6 +78,7 @@ typedef struct chiaki_connect_info_t ChiakiConnectVideoProfile video_profile; bool video_profile_auto_downgrade; // Downgrade video_profile if server does not seem to support it. bool enable_keyboard; + bool enable_dualsense; } ChiakiConnectInfo; @@ -121,6 +122,14 @@ typedef struct chiaki_rumble_event_t uint8_t right; // high-frequency } ChiakiRumbleEvent; +typedef struct chiaki_trigger_effects_event_t +{ + uint8_t type_left; + uint8_t type_right; + uint8_t left[10]; + uint8_t right[10]; +} ChiakiTriggerEffectsEvent; + typedef enum { CHIAKI_EVENT_CONNECTED, CHIAKI_EVENT_LOGIN_PIN_REQUEST, @@ -129,6 +138,7 @@ typedef enum { CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE, CHIAKI_EVENT_RUMBLE, CHIAKI_EVENT_QUIT, + CHIAKI_EVENT_TRIGGER_EFFECTS, } ChiakiEventType; typedef struct chiaki_event_t @@ -139,6 +149,7 @@ typedef struct chiaki_event_t ChiakiQuitEvent quit; ChiakiKeyboardEvent keyboard; ChiakiRumbleEvent rumble; + ChiakiTriggerEffectsEvent trigger_effects; struct { bool pin_incorrect; // false on first request, true if the pin entered before was incorrect @@ -170,6 +181,7 @@ typedef struct chiaki_session_t ChiakiConnectVideoProfile video_profile; bool video_profile_auto_downgrade; bool enable_keyboard; + bool enable_dualsense; } connect_info; ChiakiTarget target; @@ -191,6 +203,7 @@ typedef struct chiaki_session_t ChiakiVideoSampleCallback video_sample_cb; void *video_sample_cb_user; ChiakiAudioSink audio_sink; + ChiakiAudioSink haptics_sink; ChiakiThread session_thread; @@ -246,6 +259,14 @@ static inline void chiaki_session_set_audio_sink(ChiakiSession *session, ChiakiA session->audio_sink = *sink; } +/** + * @param sink contents are copied + */ +static inline void chiaki_session_set_haptics_sink(ChiakiSession *session, ChiakiAudioSink *sink) +{ + session->haptics_sink = *sink; +} + #ifdef __cplusplus } #endif diff --git a/lib/include/chiaki/streamconnection.h b/lib/include/chiaki/streamconnection.h index ba1817a..90578c6 100644 --- a/lib/include/chiaki/streamconnection.h +++ b/lib/include/chiaki/streamconnection.h @@ -32,6 +32,7 @@ typedef struct chiaki_stream_connection_t ChiakiPacketStats packet_stats; ChiakiAudioReceiver *audio_receiver; ChiakiVideoReceiver *video_receiver; + ChiakiAudioReceiver *haptics_receiver; ChiakiFeedbackSender feedback_sender; /** diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index 1715842..15d62e5 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -27,7 +27,8 @@ extern "C" { typedef enum chiaki_takion_message_data_type_t { CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF = 0, CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE = 7, - CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9 + CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9, + CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS = 11, } ChiakiTakionMessageDataType; typedef struct chiaki_takion_av_packet_t @@ -36,6 +37,7 @@ typedef struct chiaki_takion_av_packet_t ChiakiSeqNum16 frame_index; bool uses_nalu_info_structs; bool is_video; + bool is_haptics; ChiakiSeqNum16 unit_index; uint16_t units_in_frame_total; // source + units_in_frame_fec uint16_t units_in_frame_fec; @@ -46,8 +48,6 @@ typedef struct chiaki_takion_av_packet_t uint64_t key_pos; - uint8_t byte_before_audio_data; - uint8_t *data; // not owned size_t data_size; } ChiakiTakionAVPacket; @@ -106,6 +106,7 @@ typedef struct chiaki_takion_connect_info_t ChiakiTakionCallback cb; void *cb_user; bool enable_crypt; + bool enable_dualsense; uint8_t protocol_version; } ChiakiTakionConnectInfo; @@ -162,6 +163,8 @@ typedef struct chiaki_takion_t ChiakiTakionAVPacketParse av_packet_parse; ChiakiKeyState key_state; + + bool enable_dualsense; } ChiakiTakion; diff --git a/lib/protobuf/takion.proto b/lib/protobuf/takion.proto index 748d70a..ceef0f1 100644 --- a/lib/protobuf/takion.proto +++ b/lib/protobuf/takion.proto @@ -312,7 +312,8 @@ message ControllerConnectionPayload { VITA = 3; XINPUT = 4; MOBILE = 5; - BOND = 6; + DUALSENSE = 6; + VR2SENSE = 7; } } diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 9fec69d..744d807 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -5,7 +5,7 @@ #include -static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size); +static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size); CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, ChiakiSession *session, ChiakiPacketStats *packet_stats) { @@ -102,14 +102,14 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re frame_index = packet->frame_index - fec_units_count + fec_index; } - chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->data + unit_size * i, unit_size); + chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->is_haptics, packet->data + unit_size * i, unit_size); } if(audio_receiver->packet_stats) chiaki_packet_stats_push_seq(audio_receiver->packet_stats, packet->frame_index); } -static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size) +static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size) { chiaki_mutex_lock(&audio_receiver->mutex); @@ -117,7 +117,9 @@ static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, Chi goto beach; audio_receiver->frame_index_prev = frame_index; - if(audio_receiver->session->audio_sink.frame_cb) + if(is_haptics && audio_receiver->session->haptics_sink.frame_cb) + audio_receiver->session->haptics_sink.frame_cb(buf, buf_size, audio_receiver->session->haptics_sink.user); + else if(!is_haptics && audio_receiver->session->audio_sink.frame_cb) audio_receiver->session->audio_sink.frame_cb(buf, buf_size, audio_receiver->session->audio_sink.user); beach: diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index 9dfc277..29a39b2 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -488,17 +488,25 @@ static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t * static void ctrl_enable_optional_features(ChiakiCtrl *ctrl) { - if(!ctrl->session->connect_info.enable_keyboard) - return; - // TODO: Last byte of pre_enable request is random (?) - // TODO: Signature ?! - uint8_t enable = 1; - uint8_t pre_enable[4] = { 0x00, 0x01, 0x01, 0x80 }; - uint8_t signature[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - ctrl_message_send(ctrl, 0xD, signature, 0x10); - ctrl_message_send(ctrl, 0x36, pre_enable, 4); - ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1); - ctrl_message_send(ctrl, 0x36, pre_enable, 4); + if(ctrl->session->connect_info.enable_dualsense) + { + CHIAKI_LOGI(ctrl->session->log, "Enabling DualSense features"); + const uint8_t enable[3] = { 0x00, 0x40, 0x00 }; + ctrl_message_send(ctrl, 0x13, enable, 3); + } + if(ctrl->session->connect_info.enable_keyboard) + { + CHIAKI_LOGI(ctrl->session->log, "Enabling Keyboard"); + // TODO: Last byte of pre_enable request is random (?) + // TODO: Signature ?! + uint8_t enable = 1; + uint8_t pre_enable[4] = { 0x00, 0x01, 0x01, 0x80 }; + uint8_t signature[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + ctrl_message_send(ctrl, 0xD, signature, 0x10); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); + ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); + } } static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index 1ae190e..d585a88 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -76,12 +76,12 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS *((chiaki_unaligned_uint16_t *)(buf + 0x17)) = htons((uint16_t)state->right_y); } -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense) { chiaki_feedback_state_format_v9(buf, state); buf[0x19] = 0x0; buf[0x1a] = 0x0; - buf[0x1b] = 0x1; // 1 for Shock, 0 for Sense + buf[0x1b] = enable_dualsense ? 0x0 : 0x1; } CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state) diff --git a/lib/src/session.c b/lib/src/session.c index b5b928f..ea8e09d 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -227,6 +227,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki session->connect_info.video_profile = connect_info->video_profile; session->connect_info.video_profile_auto_downgrade = connect_info->video_profile_auto_downgrade; session->connect_info.enable_keyboard = connect_info->enable_keyboard; + session->connect_info.enable_dualsense = connect_info->enable_dualsense; return CHIAKI_ERR_SUCCESS; error_stop_pipe: diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index 2a7bb6f..7187c1b 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -47,7 +47,9 @@ static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user); static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size); static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream_connection); +static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection); static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection *stream_connection); static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); @@ -79,6 +81,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnecti stream_connection->video_receiver = NULL; stream_connection->audio_receiver = NULL; + stream_connection->haptics_receiver = NULL; err = chiaki_mutex_init(&stream_connection->feedback_sender_mutex, false); if(err != CHIAKI_ERR_SUCCESS) @@ -143,6 +146,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio takion_info.ip_dontfrag = false; takion_info.enable_crypt = true; + takion_info.enable_dualsense = session->connect_info.enable_dualsense; takion_info.protocol_version = chiaki_target_is_ps5(session->target) ? 12 : 9; takion_info.cb = stream_connection_takion_cb; @@ -164,12 +168,20 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio return CHIAKI_ERR_UNKNOWN; } + stream_connection->haptics_receiver = chiaki_audio_receiver_new(session, NULL); + if(!stream_connection->haptics_receiver) + { + CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Haptics Receiver"); + err = CHIAKI_ERR_UNKNOWN; + goto err_audio_receiver; + } + stream_connection->video_receiver = chiaki_video_receiver_new(session, &stream_connection->packet_stats); if(!stream_connection->video_receiver) { CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Video Receiver"); err = CHIAKI_ERR_UNKNOWN; - goto err_audio_receiver; + goto err_haptics_receiver; } stream_connection->state = STATE_TAKION_CONNECT; @@ -321,6 +333,10 @@ err_video_receiver: chiaki_video_receiver_free(stream_connection->video_receiver); stream_connection->video_receiver = NULL; +err_haptics_receiver: + chiaki_audio_receiver_free(stream_connection->haptics_receiver); + stream_connection->haptics_receiver = NULL; + err_audio_receiver: chiaki_audio_receiver_free(stream_connection->audio_receiver); stream_connection->audio_receiver = NULL; @@ -376,6 +392,9 @@ static void stream_connection_takion_data(ChiakiStreamConnection *stream_connect case CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE: stream_connection_takion_data_rumble(stream_connection, buf, buf_size); break; + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS: + stream_connection_takion_data_trigger_effects(stream_connection, buf, buf_size); + break; default: break; } @@ -415,6 +434,24 @@ static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_ chiaki_session_send_event(stream_connection->session, &event); } + +static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ + if(buf_size < 25) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection got trigger effects packet with size %#llx < 25", + (unsigned long long)buf_size); + return; + } + ChiakiEvent event = { 0 }; + event.type = CHIAKI_EVENT_TRIGGER_EFFECTS; + event.trigger_effects.type_left = buf[1]; + event.trigger_effects.type_right = buf[2]; + memcpy(&event.trigger_effects.left, buf + 5, 10); + memcpy(&event.trigger_effects.right, buf + 15, 10); + chiaki_session_send_event(stream_connection->session, &event); +} + static void stream_connection_takion_data_handle_disconnect(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) { tkproto_TakionMessage msg; @@ -460,7 +497,7 @@ static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_co return; } - CHIAKI_LOGV(stream_connection->log, "StreamConnection received data"); + CHIAKI_LOGV(stream_connection->log, "StreamConnection received data with msg.type == %d", msg.type); chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size); if(msg.type == tkproto_TakionMessage_PayloadType_DISCONNECT) @@ -522,7 +559,8 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st return; } - CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else"); + CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else: %d", msg.type); + chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size); return; } @@ -584,6 +622,12 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st // stream_connection->state_mutex is expected to be locked by the caller of this function stream_connection->state_finished = true; chiaki_cond_signal(&stream_connection->state_cond); + err = stream_connection_send_controller_connection(stream_connection); + if(err != CHIAKI_ERR_SUCCESS) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection failed to send controller connection"); + goto error; + } return; error: stream_connection->state_failed = true; @@ -811,6 +855,37 @@ static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream return err; } +static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection) +{ + ChiakiSession *session = stream_connection->session; + tkproto_TakionMessage msg; + memset(&msg, 0, sizeof(msg)); + + msg.type = tkproto_TakionMessage_PayloadType_CONTROLLERCONNECTION; + msg.has_controller_connection_payload = true; + msg.controller_connection_payload.has_connected = true; + msg.controller_connection_payload.connected = true; + msg.controller_connection_payload.has_controller_id = false; + msg.controller_connection_payload.has_controller_type = true; + msg.controller_connection_payload.controller_type = session->connect_info.enable_dualsense + ? tkproto_ControllerConnectionPayload_ControllerType_DUALSENSE + : tkproto_ControllerConnectionPayload_ControllerType_DUALSHOCK4; + + uint8_t buf[2048]; + size_t buf_size; + + pb_ostream_t stream = pb_ostream_from_buffer(buf, sizeof(buf)); + bool pbr = pb_encode(&stream, tkproto_TakionMessage_fields, &msg); + if(!pbr) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection controller connection protobuf encoding failed"); + return CHIAKI_ERR_UNKNOWN; + } + + buf_size = stream.bytes_written; + return chiaki_takion_send_message_data(&stream_connection->takion, 1, 1, buf, buf_size, NULL); +} + static ChiakiErrorCode stream_connection_send_streaminfo_ack(ChiakiStreamConnection *stream_connection) { tkproto_TakionMessage msg; @@ -867,6 +942,8 @@ static void stream_connection_takion_av(ChiakiStreamConnection *stream_connectio if(packet->is_video) chiaki_video_receiver_av_packet(stream_connection->video_receiver, packet); + else if(packet->is_haptics) + chiaki_audio_receiver_av_packet(stream_connection->haptics_receiver, packet); else chiaki_audio_receiver_av_packet(stream_connection->audio_receiver, packet); } diff --git a/lib/src/takion.c b/lib/src/takion.c index c433977..f786d6f 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -57,7 +57,8 @@ typedef enum takion_packet_type_t { TAKION_PACKET_TYPE_CONGESTION = 5, TAKION_PACKET_TYPE_FEEDBACK_STATE = 6, TAKION_PACKET_TYPE_CLIENT_INFO = 8, - TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9 + TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9, + TAKION_PACKET_TYPE_PAD_ADAPTIVE_TRIGGERS = 11, } TakionPacketType; /** @@ -215,6 +216,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_connect(ChiakiTakion *takion, Chiaki takion->postponed_packets = NULL; takion->postponed_packets_size = 0; takion->postponed_packets_count = 0; + takion->enable_dualsense = info->enable_dualsense; CHIAKI_LOGI(takion->log, "Takion connecting (version %u)", (unsigned int)info->protocol_version); @@ -556,7 +558,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_state(ChiakiTakion *ta else { buf_sz = 0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12; - chiaki_feedback_state_format_v12(buf + 0xc, feedback_state); + chiaki_feedback_state_format_v12(buf + 0xc, feedback_state, takion->enable_dualsense); } return takion_send_feedback_packet(takion, buf, buf_sz); } @@ -950,6 +952,7 @@ static void takion_flush_data_queue(ChiakiTakion *takion) if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) { CHIAKI_LOGW(takion->log, "Takion received data with unexpected data type %#x", data_type); @@ -1308,7 +1311,7 @@ static ChiakiErrorCode av_packet_parse(bool v12, ChiakiTakionAVPacket *packet, C if(v12 && !packet->is_video) { - packet->byte_before_audio_data = *av; + packet->is_haptics = *av == 0x02; av += 1; av_size -= 1; } From 801f902bea43353ea00bd70de959f499021b87d8 Mon Sep 17 00:00:00 2001 From: Street Pea Date: Sat, 10 Dec 2022 15:09:43 +0100 Subject: [PATCH 087/104] Add transform/scaling modes to GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added zoom and stretch modes to GUI to mirror the transform modes available on Android. They are reachable through a context menu or shortcuts (Ctrl+S/Ctrl+Z). CLI options --stretch and --zoom have been added as well. Co-authored-by: Florian Märkl --- gui/include/avopenglwidget.h | 11 +++- gui/include/streamsession.h | 13 ++++- gui/include/streamwindow.h | 6 ++ gui/include/transformmode.h | 12 ++++ gui/src/avopenglwidget.cpp | 36 ++++++++++-- gui/src/main.cpp | 24 +++++++- gui/src/mainwindow.cpp | 9 ++- gui/src/streamsession.cpp | 15 ++++- gui/src/streamwindow.cpp | 109 ++++++++++++++++++++++++++++++----- 9 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 gui/include/transformmode.h diff --git a/gui/include/avopenglwidget.h b/gui/include/avopenglwidget.h index 6f234e9..19bd287 100644 --- a/gui/include/avopenglwidget.h +++ b/gui/include/avopenglwidget.h @@ -3,6 +3,8 @@ #ifndef CHIAKI_AVOPENGLWIDGET_H #define CHIAKI_AVOPENGLWIDGET_H +#include "transformmode.h" + #include #include @@ -74,21 +76,24 @@ class AVOpenGLWidget: public QOpenGLWidget public: static QSurfaceFormat CreateSurfaceFormat(); - explicit AVOpenGLWidget(StreamSession *session, QWidget *parent = nullptr); + explicit AVOpenGLWidget(StreamSession *session, QWidget *parent = nullptr, TransformMode transform_mode = TransformMode::Fit); ~AVOpenGLWidget() override; void SwapFrames(); AVOpenGLFrame *GetBackgroundFrame() { return &frames[1 - frame_fg]; } + void SetTransformMode(TransformMode mode) { transform_mode = mode; } + TransformMode GetTransformMode() const { return transform_mode; } + protected: + TransformMode transform_mode; void mouseMoveEvent(QMouseEvent *event) override; void initializeGL() override; void paintGL() override; - private slots: - void ResetMouseTimeout(); public slots: + void ResetMouseTimeout(); void HideMouse(); }; diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index cb1397e..e139f45 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -20,6 +20,7 @@ #include "sessionlog.h" #include "controllermanager.h" #include "settings.h" +#include "transformmode.h" #include #include @@ -53,9 +54,17 @@ struct StreamSessionConnectInfo ChiakiConnectVideoProfile video_profile; unsigned int audio_buffer_size; bool fullscreen; + TransformMode transform_mode; bool enable_keyboard; - StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen); + StreamSessionConnectInfo( + Settings *settings, + ChiakiTarget target, + QString host, + QByteArray regist_key, + QByteArray morning, + bool fullscreen, + TransformMode transform_mode); }; class StreamSession : public QObject @@ -124,7 +133,7 @@ class StreamSession : public QObject #endif void HandleKeyboardEvent(QKeyEvent *event); - void HandleMouseEvent(QMouseEvent *event); + bool HandleMouseEvent(QMouseEvent *event); signals: void FfmpegFrameAvailable(); diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index 5c526b0..fd91d13 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -22,10 +22,14 @@ class StreamWindow: public QMainWindow const StreamSessionConnectInfo connect_info; StreamSession *session; + QAction *fullscreen_action; + QAction *stretch_action; + QAction *zoom_action; AVOpenGLWidget *av_widget; void Init(); void UpdateVideoTransform(); + void UpdateTransformModeActions(); protected: void keyPressEvent(QKeyEvent *event) override; @@ -42,6 +46,8 @@ class StreamWindow: public QMainWindow void SessionQuit(ChiakiQuitReason reason, const QString &reason_str); void LoginPINRequested(bool incorrect); void ToggleFullscreen(); + void ToggleStretch(); + void ToggleZoom(); }; #endif // CHIAKI_GUI_STREAMWINDOW_H diff --git a/gui/include/transformmode.h b/gui/include/transformmode.h new file mode 100644 index 0000000..5c01d87 --- /dev/null +++ b/gui/include/transformmode.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#ifndef CHIAKI_TRANSFORMMODE_H +#define CHIAKI_TRANSFORMMODE_H + +enum class TransformMode { + Fit, + Zoom, + Stretch +}; + +#endif diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 857af09..bfd184a 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -122,9 +122,9 @@ QSurfaceFormat AVOpenGLWidget::CreateSurfaceFormat() return format; } -AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent) +AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent, TransformMode transform_mode) : QOpenGLWidget(parent), - session(session) + session(session), transform_mode(transform_mode) { enum AVPixelFormat pixel_format = chiaki_ffmpeg_decoder_get_pixel_format(session->GetFfmpegDecoder()); conversion_config = nullptr; @@ -381,10 +381,10 @@ void AVOpenGLWidget::paintGL() vp_width = widget_width; vp_height = widget_height; } - else + else if(transform_mode == TransformMode::Fit) { float aspect = (float)frame->width / (float)frame->height; - if(aspect < (float)widget_width / (float)widget_height) + if(widget_height && aspect < (float)widget_width / (float)widget_height) { vp_height = widget_height; vp_width = (GLsizei)(vp_height * aspect); @@ -395,6 +395,34 @@ void AVOpenGLWidget::paintGL() vp_height = (GLsizei)(vp_width / aspect); } } + else if(transform_mode == TransformMode::Zoom) + { + float aspect = (float)frame->width / (float)frame->height; + if(widget_height && aspect < (float)widget_width / (float)widget_height) + { + vp_width = widget_width; + vp_height = (GLsizei)(vp_width / aspect); + } + else + { + vp_height = widget_height; + vp_width = (GLsizei)(vp_height * aspect); + } + } + else // transform_mode == TransformMode::Stretch + { + float aspect = (float)frame->width / (float)frame->height; + if(widget_height && aspect < (float)widget_width / (float)widget_height) + { + vp_height = widget_height; + vp_width = widget_width; + } + else + { + vp_width = widget_width; + vp_height = widget_height; + } + } f->glViewport((widget_width - vp_width) / 2, (widget_height - vp_height) / 2, vp_width, vp_height); diff --git a/gui/src/main.cpp b/gui/src/main.cpp index e6597cf..5f2aad7 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -103,9 +103,15 @@ int real_main(int argc, char *argv[]) QCommandLineOption morning_option("morning", "", "morning"); parser.addOption(morning_option); - QCommandLineOption fullscreen_option("fullscreen", "Start window in fullscreen (only for use with stream command)"); + QCommandLineOption fullscreen_option("fullscreen", "Start window in fullscreen mode [maintains aspect ratio, adds black bars to fill unsused parts of screen if applicable] (only for use with stream command)"); parser.addOption(fullscreen_option); + QCommandLineOption zoom_option("zoom", "Start window in fullscreen zoomed in to fit screen [maintains aspect ratio, cutting off edges of image to fill screen] (only for use with stream command)"); + parser.addOption(zoom_option); + + QCommandLineOption stretch_option("stretch", "Start window in fullscreen stretched to fit screen [distorts aspect ratio to fill screen] (only for use with stream command)"); + parser.addOption(stretch_option); + parser.process(app); QStringList args = parser.positionalArguments(); @@ -174,7 +180,21 @@ int real_main(int argc, char *argv[]) return 1; } } - StreamSessionConnectInfo connect_info(&settings, target, host, regist_key, morning, parser.isSet(fullscreen_option)); + if ((parser.isSet(stretch_option) && (parser.isSet(zoom_option) || parser.isSet(fullscreen_option))) || (parser.isSet(zoom_option) && parser.isSet(fullscreen_option))) + { + printf("Must choose between fullscreen, zoom or stretch option."); + return 1; + } + + StreamSessionConnectInfo connect_info( + &settings, + target, + host, + regist_key, + morning, + parser.isSet(fullscreen_option), + parser.isSet(zoom_option) ? TransformMode::Zoom : parser.isSet(stretch_option) ? TransformMode::Stretch : TransformMode::Fit); + return RunStream(app, connect_info); } #ifdef CHIAKI_ENABLE_CLI diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index aa76ab3..c3b1b6a 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -249,7 +249,14 @@ void MainWindow::ServerItemWidgetTriggered() } QString host = server.GetHostAddr(); - StreamSessionConnectInfo info(settings, server.registered_host.GetTarget(), host, server.registered_host.GetRPRegistKey(), server.registered_host.GetRPKey(), false); + StreamSessionConnectInfo info( + settings, + server.registered_host.GetTarget(), + host, + server.registered_host.GetRPRegistKey(), + server.registered_host.GetRPKey(), + false, + TransformMode::Fit); new StreamWindow(info); } else diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 3f46070..f5b7ace 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -14,7 +14,14 @@ #define SETSU_UPDATE_INTERVAL_MS 4 -StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen) +StreamSessionConnectInfo::StreamSessionConnectInfo( + Settings *settings, + ChiakiTarget target, + QString host, + QByteArray regist_key, + QByteArray morning, + bool fullscreen, + TransformMode transform_mode) : settings(settings) { key_map = settings->GetControllerMappingForDecoding(); @@ -30,6 +37,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTar this->morning = morning; audio_buffer_size = settings->GetAudioBufferSize(); this->fullscreen = fullscreen; + this->transform_mode = transform_mode; this->enable_keyboard = false; // TODO: from settings } @@ -228,13 +236,16 @@ void StreamSession::SetLoginPIN(const QString &pin) chiaki_session_set_login_pin(&session, (const uint8_t *)data.constData(), data.size()); } -void StreamSession::HandleMouseEvent(QMouseEvent *event) +bool StreamSession::HandleMouseEvent(QMouseEvent *event) { + if(event->button() != Qt::MouseButton::LeftButton) + return false; if(event->type() == QEvent::MouseButtonPress) keyboard_state.buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; else keyboard_state.buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; SendFeedbackState(); + return true; } void StreamSession::HandleKeyboardEvent(QKeyEvent *event) diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index feff758..d1fc4e8 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -10,6 +10,7 @@ #include #include #include +#include StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget *parent) : QMainWindow(parent), @@ -23,8 +24,6 @@ StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget try { - if(connect_info.fullscreen) - showFullScreen(); Init(); } catch(const Exception &e) @@ -40,6 +39,8 @@ StreamWindow::~StreamWindow() delete av_widget; } +#include + void StreamWindow::Init() { session = new StreamSession(connect_info, this); @@ -47,10 +48,36 @@ void StreamWindow::Init() connect(session, &StreamSession::SessionQuit, this, &StreamWindow::SessionQuit); connect(session, &StreamSession::LoginPINRequested, this, &StreamWindow::LoginPINRequested); + const QKeySequence fullscreen_shortcut = Qt::Key_F11; + const QKeySequence stretch_shortcut = Qt::CTRL + Qt::Key_S; + const QKeySequence zoom_shortcut = Qt::CTRL + Qt::Key_Z; + + fullscreen_action = new QAction(tr("Fullscreen"), this); + fullscreen_action->setCheckable(true); + fullscreen_action->setShortcut(fullscreen_shortcut); + addAction(fullscreen_action); + connect(fullscreen_action, &QAction::triggered, this, &StreamWindow::ToggleFullscreen); + if(session->GetFfmpegDecoder()) { - av_widget = new AVOpenGLWidget(session, this); + av_widget = new AVOpenGLWidget(session, this, connect_info.transform_mode); setCentralWidget(av_widget); + + av_widget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(av_widget, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) { + av_widget->ResetMouseTimeout(); + + QMenu menu(av_widget); + menu.addAction(fullscreen_action); + menu.addSeparator(); + menu.addAction(stretch_action); + menu.addAction(zoom_action); + releaseKeyboard(); + connect(&menu, &QMenu::aboutToHide, this, [this] { + grabKeyboard(); + }); + menu.exec(av_widget->mapToGlobal(pos)); + }); } else { @@ -63,13 +90,29 @@ void StreamWindow::Init() session->Start(); - auto fullscreen_action = new QAction(tr("Fullscreen"), this); - fullscreen_action->setShortcut(Qt::Key_F11); - addAction(fullscreen_action); - connect(fullscreen_action, &QAction::triggered, this, &StreamWindow::ToggleFullscreen); + stretch_action = new QAction(tr("Stretch"), this); + stretch_action->setCheckable(true); + stretch_action->setShortcut(stretch_shortcut); + addAction(stretch_action); + connect(stretch_action, &QAction::triggered, this, &StreamWindow::ToggleStretch); + + zoom_action = new QAction(tr("Zoom"), this); + zoom_action->setCheckable(true); + zoom_action->setShortcut(zoom_shortcut); + addAction(zoom_action); + connect(zoom_action, &QAction::triggered, this, &StreamWindow::ToggleZoom); resize(connect_info.video_profile.width, connect_info.video_profile.height); - show(); + + if(connect_info.fullscreen) + { + showFullScreen(); + fullscreen_action->setChecked(true); + } + else + show(); + + UpdateTransformModeActions(); } void StreamWindow::keyPressEvent(QKeyEvent *event) @@ -86,20 +129,25 @@ void StreamWindow::keyReleaseEvent(QKeyEvent *event) void StreamWindow::mousePressEvent(QMouseEvent *event) { - if(session) - session->HandleMouseEvent(event); + if(session && session->HandleMouseEvent(event)) + return; + QMainWindow::mousePressEvent(event); } void StreamWindow::mouseReleaseEvent(QMouseEvent *event) { - if(session) - session->HandleMouseEvent(event); + if(session && session->HandleMouseEvent(event)) + return; + QMainWindow::mouseReleaseEvent(event); } void StreamWindow::mouseDoubleClickEvent(QMouseEvent *event) { - ToggleFullscreen(); - + if(event->button() == Qt::MouseButton::LeftButton) + { + ToggleFullscreen(); + return; + } QMainWindow::mouseDoubleClickEvent(event); } @@ -175,15 +223,48 @@ void StreamWindow::LoginPINRequested(bool incorrect) void StreamWindow::ToggleFullscreen() { if(isFullScreen()) + { showNormal(); + fullscreen_action->setChecked(false); + } else { showFullScreen(); if(av_widget) av_widget->HideMouse(); + fullscreen_action->setChecked(true); } } +void StreamWindow::UpdateTransformModeActions() +{ + TransformMode tm = av_widget ? av_widget->GetTransformMode() : TransformMode::Fit; + stretch_action->setChecked(tm == TransformMode::Stretch); + zoom_action->setChecked(tm == TransformMode::Zoom); +} + +void StreamWindow::ToggleStretch() +{ + if(!av_widget) + return; + av_widget->SetTransformMode( + av_widget->GetTransformMode() == TransformMode::Stretch + ? TransformMode::Fit + : TransformMode::Stretch); + UpdateTransformModeActions(); +} + +void StreamWindow::ToggleZoom() +{ + if(!av_widget) + return; + av_widget->SetTransformMode( + av_widget->GetTransformMode() == TransformMode::Zoom + ? TransformMode::Fit + : TransformMode::Zoom); + UpdateTransformModeActions(); +} + void StreamWindow::resizeEvent(QResizeEvent *event) { UpdateVideoTransform(); From 36816db7acd80f88d7070ce9a66cf702f4ad6265 Mon Sep 17 00:00:00 2001 From: Street Pea Date: Sat, 10 Dec 2022 15:12:28 +0100 Subject: [PATCH 088/104] Add quit (Ctrl+Q) shortcut to GUI --- gui/include/mainwindow.h | 1 + gui/include/streamwindow.h | 1 + gui/src/mainwindow.cpp | 10 ++++++++++ gui/src/streamwindow.cpp | 10 ++++++++++ 4 files changed, 22 insertions(+) diff --git a/gui/include/mainwindow.h b/gui/include/mainwindow.h index f1190de..4112c19 100644 --- a/gui/include/mainwindow.h +++ b/gui/include/mainwindow.h @@ -55,6 +55,7 @@ class MainWindow : public QMainWindow void UpdateDiscoveryEnabled(); void ShowSettings(); + void Quit(); void UpdateDisplayServers(); void UpdateServerWidgets(); diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index fd91d13..f3bd8bb 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -48,6 +48,7 @@ class StreamWindow: public QMainWindow void ToggleFullscreen(); void ToggleStretch(); void ToggleZoom(); + void Quit(); }; #endif // CHIAKI_GUI_STREAMWINDOW_H diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index c3b1b6a..0912b2e 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -146,6 +146,11 @@ MainWindow::MainWindow(Settings *settings, QWidget *parent) AddToolBarAction(settings_action); connect(settings_action, &QAction::triggered, this, &MainWindow::ShowSettings); + auto quit_action = new QAction(tr("Quit"), this); + quit_action->setShortcut(Qt::CTRL + Qt::Key_Q); + addAction(quit_action); + connect(quit_action, &QAction::triggered, this, &MainWindow::Quit); + auto scroll_area = new QScrollArea(this); scroll_area->setWidgetResizable(true); scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -305,6 +310,11 @@ void MainWindow::ShowSettings() dialog.exec(); } +void MainWindow::Quit() +{ + qApp->exit(); +} + void MainWindow::UpdateDisplayServers() { display_servers.clear(); diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index d1fc4e8..d1c0b95 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -102,6 +102,11 @@ void StreamWindow::Init() addAction(zoom_action); connect(zoom_action, &QAction::triggered, this, &StreamWindow::ToggleZoom); + auto quit_action = new QAction(tr("Quit"), this); + quit_action->setShortcut(Qt::CTRL + Qt::Key_Q); + addAction(quit_action); + connect(quit_action, &QAction::triggered, this, &StreamWindow::Quit); + resize(connect_info.video_profile.width, connect_info.video_profile.height); if(connect_info.fullscreen) @@ -127,6 +132,11 @@ void StreamWindow::keyReleaseEvent(QKeyEvent *event) session->HandleKeyboardEvent(event); } +void StreamWindow::Quit() +{ + close(); +} + void StreamWindow::mousePressEvent(QMouseEvent *event) { if(session && session->HandleMouseEvent(event)) From 76690a319cb5d24005561cd5996c59f96ebe2778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 14 Dec 2022 18:02:23 +0100 Subject: [PATCH 089/104] Fix testing on AppVeyor and make appveyor-win.sh more portable test/chiaki-unit.exe failed to load some OpenSSL dlls somehow, which broke the build. Moving them next to the executable fixes that. The APPVEYOR_BUILD_FOLDER env var is also not needed anymore now. --- scripts/appveyor-win.sh | 81 ++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index 3951d84..a7e63a0 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -1,77 +1,90 @@ #!/bin/bash -echo "APPVEYOR_BUILD_FOLDER=$APPVEYOR_BUILD_FOLDER" +set -xe -mkdir ninja && cd ninja || exit 1 -wget https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip && 7z x ninja-win.zip || exit 1 -cd .. || exit 1 +BUILD_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" +BUILD_ROOT="$(echo $BUILD_ROOT | sed 's|^/\([a-z]\)|\1:|g')" # replace /c/... by c:/... for cmake to understand it +echo "BUILD_ROOT=$BUILD_ROOT" -mkdir yasm && cd yasm || exit 1 -wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe && mv yasm-1.3.0-win64.exe yasm.exe || exit 1 -cd .. || exit 1 +mkdir ninja && cd ninja +wget https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip && 7z x ninja-win.zip +cd .. + +mkdir yasm && cd yasm +wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe && mv yasm-1.3.0-win64.exe yasm.exe +cd .. export PATH="$PWD/ninja:$PWD/yasm:/c/Qt/5.12/msvc2017_64/bin:$PATH" -scripts/build-ffmpeg.sh . --target-os=win64 --arch=x86_64 --toolchain=msvc || exit 1 +scripts/build-ffmpeg.sh . --target-os=win64 --arch=x86_64 --toolchain=msvc -git clone https://github.com/xiph/opus.git && cd opus && git checkout ad8fe90db79b7d2a135e3dfd2ed6631b0c5662ab || exit 1 -mkdir build && cd build || exit 1 +git clone https://github.com/xiph/opus.git && cd opus && git checkout ad8fe90db79b7d2a135e3dfd2ed6631b0c5662ab +mkdir build && cd build cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX="$APPVEYOR_BUILD_FOLDER/opus-prefix" \ - .. || exit 1 -ninja || exit 1 -ninja install || exit 1 -cd ../.. || exit 1 + -DCMAKE_INSTALL_PREFIX="$BUILD_ROOT/opus-prefix" \ + .. +ninja +ninja install +cd ../.. -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip -wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 -export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 -export SDL_ROOT=${SDL_ROOT//[\\]//} || exit 1 +wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip +export SDL_ROOT="$BUILD_ROOT/SDL2-2.0.10" +export SDL_ROOT=${SDL_ROOT//[\\]//} echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\") set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\") -set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake" || exit 1 +set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake" -mkdir protoc && cd protoc || exit 1 -wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip || exit 1 -cd .. || exit 1 -export PATH="$PWD/protoc/bin:$PATH" || exit 1 +mkdir protoc && cd protoc +wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip +cd .. +export PATH="$PWD/protoc/bin:$PATH" PYTHON="C:/Python37/python.exe" -"$PYTHON" -m pip install protobuf==3.19.5 || exit 1 +"$PYTHON" -m pip install protobuf==3.19.5 QT_PATH="C:/Qt/5.15/msvc2019_64" COPY_DLLS="$PWD/openssl-1.1/x64/bin/libcrypto-1_1-x64.dll $PWD/openssl-1.1/x64/bin/libssl-1_1-x64.dll $SDL_ROOT/lib/x64/SDL2.dll" -mkdir build && cd build || exit 1 +echo "-- Configure" + +mkdir build && cd build cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ -DCMAKE_C_FLAGS="-we4013" \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_PREFIX_PATH="$APPVEYOR_BUILD_FOLDER/ffmpeg-prefix;$APPVEYOR_BUILD_FOLDER/opus-prefix;$APPVEYOR_BUILD_FOLDER/openssl-1.1/x64;$QT_PATH;$SDL_ROOT" \ + -DCMAKE_PREFIX_PATH="$BUILD_ROOT/ffmpeg-prefix;$BUILD_ROOT/opus-prefix;$BUILD_ROOT/openssl-1.1/x64;$QT_PATH;$SDL_ROOT" \ -DPYTHON_EXECUTABLE="$PYTHON" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ - .. || exit 1 + .. -ninja || exit 1 +echo "-- Build" -test/chiaki-unit.exe || exit 1 +ninja -cd .. || exit 1 +echo "-- Test" + +cp $COPY_DLLS test/ +test/chiaki-unit.exe + +cd .. # Deploy -mkdir Chiaki && cp build/gui/chiaki.exe Chiaki || exit 1 -mkdir Chiaki-PDB && cp build/gui/chiaki.pdb Chiaki-PDB || exit 1 +echo "-- Deploy" -"$QT_PATH/bin/windeployqt.exe" Chiaki/chiaki.exe || exit 1 +mkdir Chiaki && cp build/gui/chiaki.exe Chiaki +mkdir Chiaki-PDB && cp build/gui/chiaki.pdb Chiaki-PDB + +"$QT_PATH/bin/windeployqt.exe" Chiaki/chiaki.exe cp -v $COPY_DLLS Chiaki From 4c8209762c1f822d2066367062170f47692abd54 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Mon, 7 Nov 2022 22:50:32 +0100 Subject: [PATCH 090/104] Add support for touchpad and sensor handling via SDL This should enable support for more controllers besides the DS4 and DualSense, basically any controller supported by SDL that has at least one touchpad, an accelerometer and a gyroscope. Older SDL versions have been tested down to 2.0.9. Versions older than 2.0.14 won't have sensors and touchpad support, though. Setsu is deprecated and remains in-tree for now, but defaults to being disabled if SDL2 is found and >= 2.0.14. If Setsu is enabled explicitly, touchpad and sensors are not handled by SDL. --- CMakeLists.txt | 36 ++-- cmake/FindSDL2.cmake | 20 +++ gui/CMakeLists.txt | 3 - gui/include/controllermanager.h | 21 ++- gui/src/controllermanager.cpp | 254 +++++++++++++++++++++++---- lib/src/controller.c | 13 ++ scripts/build-appimage.sh | 1 - scripts/build-sdl2.sh | 7 +- scripts/run-podman-build-bullseye.sh | 2 +- 9 files changed, 298 insertions(+), 59 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a552a0..3af7f5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,21 +150,30 @@ if(CHIAKI_ENABLE_CLI) add_subdirectory(cli) endif() +if(CHIAKI_ENABLE_GUI AND CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) + find_package(SDL2 MODULE REQUIRED) +endif() + if(CHIAKI_ENABLE_SETSU) - find_package(Udev QUIET) - find_package(Evdev QUIET) - if(Udev_FOUND AND Evdev_FOUND) - set(CHIAKI_ENABLE_SETSU ON) - else() - if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO) - message(FATAL_ERROR " -CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved. -Keep in mind that setsu is only supported on Linux!") - endif() + if(CHIAKI_ENABLE_SETSU STREQUAL AUTO AND SDL2_FOUND AND (SDL2_VERSION_MINOR GREATER 0 OR SDL2_VERSION_PATCH GREATER_EQUAL 14)) + message(STATUS "SDL version ${SDL2_VERSION} is >= 2.0.14, disabling Setsu") set(CHIAKI_ENABLE_SETSU OFF) - endif() - if(CHIAKI_ENABLE_SETSU) - add_subdirectory(setsu) + else() + find_package(Udev QUIET) + find_package(Evdev QUIET) + if(Udev_FOUND AND Evdev_FOUND) + set(CHIAKI_ENABLE_SETSU ON) + else() + if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO) + message(FATAL_ERROR " + CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved. + Keep in mind that setsu is only supported on Linux!") + endif() + set(CHIAKI_ENABLE_SETSU OFF) + endif() + if(CHIAKI_ENABLE_SETSU) + add_subdirectory(setsu) + endif() endif() endif() @@ -175,7 +184,6 @@ else() endif() if(CHIAKI_ENABLE_GUI) - #add_subdirectory(setsu) add_subdirectory(gui) endif() diff --git a/cmake/FindSDL2.cmake b/cmake/FindSDL2.cmake index a0b9ebd..03717b0 100644 --- a/cmake/FindSDL2.cmake +++ b/cmake/FindSDL2.cmake @@ -1,6 +1,26 @@ find_package(SDL2 NO_MODULE QUIET) +# Adapted from libsdl-org/SDL_ttf: https://github.com/libsdl-org/SDL_ttf/blob/main/cmake/FindPrivateSDL2.cmake#L19-L31 +# Copyright (C) 1997-2022 Sam Lantinga +# Licensed under the zlib license (https://github.com/libsdl-org/SDL_ttf/blob/main/LICENSE.txt) +set(SDL2_VERSION_MAJOR) +set(SDL2_VERSION_MINOR) +set(SDL2_VERSION_PATCH) +set(SDL2_VERSION) +if(SDL2_INCLUDE_DIR) + file(READ "${SDL2_INCLUDE_DIR}/SDL_version.h" _sdl_version_h) + string(REGEX MATCH "#define[ \t]+SDL_MAJOR_VERSION[ \t]+([0-9]+)" _sdl2_major_re "${_sdl_version_h}") + set(SDL2_VERSION_MAJOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define[ \t]+SDL_MINOR_VERSION[ \t]+([0-9]+)" _sdl2_minor_re "${_sdl_version_h}") + set(SDL2_VERSION_MINOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define[ \t]+SDL_PATCHLEVEL[ \t]+([0-9]+)" _sdl2_patch_re "${_sdl_version_h}") + set(SDL2_VERSION_PATCH "${CMAKE_MATCH_1}") + if(_sdl2_major_re AND _sdl2_minor_re AND _sdl2_patch_re) + set(SDL2_VERSION "${SDL2_VERSION_MAJOR}.${SDL2_VERSION_MINOR}.${SDL2_VERSION_PATCH}") + endif() +endif() + if(SDL2_FOUND AND (NOT TARGET SDL2::SDL2)) add_library(SDL2::SDL2 UNKNOWN IMPORTED GLOBAL) if(NOT SDL2_LIBDIR) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 5f44908..5e51b55 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -6,9 +6,6 @@ find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Concurrent Multimedia Open if(APPLE) find_package(Qt5 REQUIRED COMPONENTS MacExtras) endif() -if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) - find_package(SDL2 MODULE REQUIRED) -endif() if(WIN32) add_definitions(-DWIN32_LEAN_AND_MEAN) diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 18d9d72..9e89fcc 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -12,8 +12,12 @@ #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER #include +#include #endif +#define PS_TOUCHPAD_MAX_X 1920 +#define PS_TOUCHPAD_MAX_Y 1079 + class Controller; class ControllerManager : public QObject @@ -33,7 +37,9 @@ class ControllerManager : public QObject private slots: void UpdateAvailableControllers(); void HandleEvents(); - void ControllerEvent(int device_id); +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + void ControllerEvent(SDL_Event evt); +#endif public: static ControllerManager *GetInstance(); @@ -57,12 +63,23 @@ class Controller : public QObject private: Controller(int device_id, ControllerManager *manager); - void UpdateState(); +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + void UpdateState(SDL_Event event); + bool HandleButtonEvent(SDL_ControllerButtonEvent event); + bool HandleAxisEvent(SDL_ControllerAxisEvent event); +#if SDL_VERSION_ATLEAST(2, 0, 14) + bool HandleSensorEvent(SDL_ControllerSensorEvent event); + bool HandleTouchpadEvent(SDL_ControllerTouchpadEvent event); +#endif +#endif ControllerManager *manager; int id; + ChiakiOrientationTracker orientation_tracker; + ChiakiControllerState state; #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + QMap, uint8_t> touch_ids; SDL_GameController *controller; #endif diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 667b736..b1f38e7 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -156,22 +156,51 @@ void ControllerManager::HandleEvents() break; case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONDOWN: - ControllerEvent(event.cbutton.which); - break; case SDL_CONTROLLERAXISMOTION: - ControllerEvent(event.caxis.which); +#if not defined(CHIAKI_ENABLE_SETSU) and SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: +#endif + ControllerEvent(event); break; } } #endif } -void ControllerManager::ControllerEvent(int device_id) +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +void ControllerManager::ControllerEvent(SDL_Event event) { + int device_id; + switch(event.type) + { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + device_id = event.cbutton.which; + break; + case SDL_CONTROLLERAXISMOTION: + device_id = event.caxis.which; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + device_id = event.csensor.which; + break; + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: + device_id = event.ctouchpad.which; + break; +#endif + default: + return; + } if(!open_controllers.contains(device_id)) return; - open_controllers[device_id]->UpdateState(); + open_controllers[device_id]->UpdateState(event); } +#endif QSet ControllerManager::GetAvailableControllers() { @@ -200,6 +229,8 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana { this->id = device_id; this->manager = manager; + chiaki_orientation_tracker_init(&this->orientation_tracker); + chiaki_controller_state_set_idle(&this->state); #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER controller = nullptr; @@ -208,7 +239,13 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana if(SDL_JoystickGetDeviceInstanceID(i) == device_id) { controller = SDL_GameControllerOpen(i); +#if SDL_VERSION_ATLEAST(2, 0, 14) + if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL)) + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO)) + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); break; +#endif } } #endif @@ -223,11 +260,187 @@ Controller::~Controller() manager->ControllerClosed(this); } -void Controller::UpdateState() +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +void Controller::UpdateState(SDL_Event event) { + switch(event.type) + { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if(!HandleButtonEvent(event.cbutton)) + return; + break; + case SDL_CONTROLLERAXISMOTION: + if(!HandleAxisEvent(event.caxis)) + return; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + if(!HandleSensorEvent(event.csensor)) + return; + break; + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: + if(!HandleTouchpadEvent(event.ctouchpad)) + return; + break; +#endif + default: + return; + + } emit StateChanged(); } +inline bool Controller::HandleButtonEvent(SDL_ControllerButtonEvent event) { + ChiakiControllerButton ps_btn; + switch(event.button) + { + case SDL_CONTROLLER_BUTTON_A: + ps_btn = CHIAKI_CONTROLLER_BUTTON_CROSS; + break; + case SDL_CONTROLLER_BUTTON_B: + ps_btn = CHIAKI_CONTROLLER_BUTTON_MOON; + break; + case SDL_CONTROLLER_BUTTON_X: + ps_btn = CHIAKI_CONTROLLER_BUTTON_BOX; + break; + case SDL_CONTROLLER_BUTTON_Y: + ps_btn = CHIAKI_CONTROLLER_BUTTON_PYRAMID; + break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; + break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; + break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_UP; + break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; + break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + ps_btn = CHIAKI_CONTROLLER_BUTTON_L1; + break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + ps_btn = CHIAKI_CONTROLLER_BUTTON_R1; + break; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_L3; + break; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_R3; + break; + case SDL_CONTROLLER_BUTTON_START: + ps_btn = CHIAKI_CONTROLLER_BUTTON_OPTIONS; + break; + case SDL_CONTROLLER_BUTTON_BACK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_SHARE; + break; + case SDL_CONTROLLER_BUTTON_GUIDE: + ps_btn = CHIAKI_CONTROLLER_BUTTON_PS; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLER_BUTTON_TOUCHPAD: + ps_btn = CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; + break; +#endif + default: + return false; + } + if(event.type == SDL_CONTROLLERBUTTONDOWN) + state.buttons |= ps_btn; + else + state.buttons &= ~ps_btn; + return true; +} + +inline bool Controller::HandleAxisEvent(SDL_ControllerAxisEvent event) { + switch(event.axis) + { + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + state.l2_state = (uint8_t)(event.value >> 7); + break; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + state.r2_state = (uint8_t)(event.value >> 7); + break; + case SDL_CONTROLLER_AXIS_LEFTX: + state.left_x = event.value; + break; + case SDL_CONTROLLER_AXIS_LEFTY: + state.left_y = event.value; + break; + case SDL_CONTROLLER_AXIS_RIGHTX: + state.right_x = event.value; + break; + case SDL_CONTROLLER_AXIS_RIGHTY: + state.right_y = event.value; + break; + default: + return false; + } + return true; +} + +#if SDL_VERSION_ATLEAST(2, 0, 14) +inline bool Controller::HandleSensorEvent(SDL_ControllerSensorEvent event) +{ + switch(event.sensor) + { + case SDL_SENSOR_ACCEL: + state.accel_x = event.data[0] / SDL_STANDARD_GRAVITY; + state.accel_y = event.data[1] / SDL_STANDARD_GRAVITY; + state.accel_z = event.data[2] / SDL_STANDARD_GRAVITY; + break; + case SDL_SENSOR_GYRO: + state.gyro_x = event.data[0]; + state.gyro_y = event.data[1]; + state.gyro_z = event.data[2]; + break; + default: + return false; + } + chiaki_orientation_tracker_update( + &orientation_tracker, state.gyro_x, state.gyro_y, state.gyro_z, + state.accel_x, state.accel_y, state.accel_z, event.timestamp * 1000); + chiaki_orientation_tracker_apply_to_controller_state(&orientation_tracker, &state); + return true; +} + +inline bool Controller::HandleTouchpadEvent(SDL_ControllerTouchpadEvent event) +{ + auto key = qMakePair(event.touchpad, event.finger); + bool exists = touch_ids.contains(key); + uint8_t chiaki_id; + switch(event.type) + { + case SDL_CONTROLLERTOUCHPADDOWN: + if(touch_ids.size() >= CHIAKI_CONTROLLER_TOUCHES_MAX) + return false; + chiaki_id = chiaki_controller_state_start_touch(&state, event.x * PS_TOUCHPAD_MAX_X, event.y * PS_TOUCHPAD_MAX_Y); + touch_ids.insert(key, chiaki_id); + break; + case SDL_CONTROLLERTOUCHPADMOTION: + if(!exists) + return false; + chiaki_controller_state_set_touch_pos(&state, touch_ids[key], event.x * PS_TOUCHPAD_MAX_X, event.y * PS_TOUCHPAD_MAX_Y); + break; + case SDL_CONTROLLERTOUCHPADUP: + if(!exists) + return false; + chiaki_controller_state_stop_touch(&state, touch_ids[key]); + touch_ids.remove(key); + break; + default: + return false; + } + return true; +} +#endif +#endif + bool Controller::IsConnected() { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER @@ -263,35 +476,6 @@ QString Controller::GetName() ChiakiControllerState Controller::GetState() { - ChiakiControllerState state; - chiaki_controller_state_set_idle(&state); -#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER - if(!controller) - return state; - - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A) ? CHIAKI_CONTROLLER_BUTTON_CROSS : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_B) ? CHIAKI_CONTROLLER_BUTTON_MOON : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_X) ? CHIAKI_CONTROLLER_BUTTON_BOX : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_Y) ? CHIAKI_CONTROLLER_BUTTON_PYRAMID : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT) ? CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) ? CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_UP) ? CHIAKI_CONTROLLER_BUTTON_DPAD_UP : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN) ? CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ? CHIAKI_CONTROLLER_BUTTON_L1 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ? CHIAKI_CONTROLLER_BUTTON_R1 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSTICK) ? CHIAKI_CONTROLLER_BUTTON_L3 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSTICK) ? CHIAKI_CONTROLLER_BUTTON_R3 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START) ? CHIAKI_CONTROLLER_BUTTON_OPTIONS : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_BACK) ? CHIAKI_CONTROLLER_BUTTON_SHARE : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_GUIDE) ? CHIAKI_CONTROLLER_BUTTON_PS : 0; - state.l2_state = (uint8_t)(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) >> 7); - state.r2_state = (uint8_t)(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7); - state.left_x = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); - state.left_y = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); - state.right_x = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX); - state.right_y = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY); - -#endif return state; } diff --git a/lib/src/controller.c b/lib/src/controller.c index 21b9971..744a83f 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -121,6 +121,19 @@ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, Chiaki out->right_x = MAX_ABS(a->right_x, b->right_x); out->right_y = MAX_ABS(a->right_y, b->right_y); + #define ORF(n, idle_val) if(a->n == idle_val) out->n = b->n; else out->n = a->n + ORF(accel_x, 0.0f); + ORF(accel_y, 1.0f); + ORF(accel_z, 0.0f); + ORF(gyro_x, 0.0f); + ORF(gyro_y, 0.0f); + ORF(gyro_z, 0.0f); + ORF(orient_x, 0.0f); + ORF(orient_y, 0.0f); + ORF(orient_z, 0.0f); + ORF(orient_w, 1.0f); + #undef ORF + out->touch_id_next = 0; for(size_t i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 83903fd..4fbd588 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -22,7 +22,6 @@ cmake \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_ENABLE_GUI=ON \ - -DCHIAKI_ENABLE_SETSU=ON \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ -DCMAKE_INSTALL_PREFIX=/usr \ .. diff --git a/scripts/build-sdl2.sh b/scripts/build-sdl2.sh index f06b9cc..725bbf4 100755 --- a/scripts/build-sdl2.sh +++ b/scripts/build-sdl2.sh @@ -6,9 +6,10 @@ cd $(dirname "${BASH_SOURCE[0]}")/.. cd "./$1" ROOT="`pwd`" -URL=https://www.libsdl.org/release/SDL2-2.0.10.tar.gz -FILE=SDL2-2.0.10.tar.gz -DIR=SDL2-2.0.10 +SDL_VER=2.26.1 +URL=https://www.libsdl.org/release/SDL2-${SDL_VER}.tar.gz +FILE=SDL2-${SDL_VER}.tar.gz +DIR=SDL2-${SDL_VER} if [ ! -d "$DIR" ]; then curl -L "$URL" -O diff --git a/scripts/run-podman-build-bullseye.sh b/scripts/run-podman-build-bullseye.sh index ad5c6d1..344a76e 100755 --- a/scripts/run-podman-build-bullseye.sh +++ b/scripts/run-podman-build-bullseye.sh @@ -9,7 +9,7 @@ podman run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " cd /build && rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py && mkdir build_bullseye && - cmake -Bbuild_bullseye -GNinja -DCHIAKI_ENABLE_SETSU=ON -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && + cmake -Bbuild_bullseye -GNinja -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && ninja -C build_bullseye && ninja -C build_bullseye test" From 7a490b5aaea1d8cd26e042eb9f681d5a05df98c5 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Mon, 21 Nov 2022 20:21:43 +0100 Subject: [PATCH 091/104] Fix feedback state position 0x1b when DualSense is connected. The previous value of `0` caused the PS5 to expect a set of 'trigger status' values in the 0x19 and 0x1a position, which would have required reading raw values from the DualSense HID device, since these values are not reported by the SDL DualSense driver (code that does so can be checked out from the `trigger-feedback` branch on https://git.sr.ht/~jbaiter/chiaki). Fortunately this is not neccessary, simply setting the value to `1` seems to make the PS5 to rely on fallback logic (presumably based on the L2/R2 value) and games that would otherwise have relied on the trigger status (Astro's Playroom climbing levels) now work without any problems. --- lib/include/chiaki/feedback.h | 2 +- lib/src/feedback.c | 8 ++++++-- lib/src/takion.c | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index d0be7a1..be66384 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -38,7 +38,7 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS /** * @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12 */ -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense); +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state); #define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x5 diff --git a/lib/src/feedback.c b/lib/src/feedback.c index d585a88..6db8364 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -76,12 +76,16 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS *((chiaki_unaligned_uint16_t *)(buf + 0x17)) = htons((uint16_t)state->right_y); } -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense) +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) { chiaki_feedback_state_format_v9(buf, state); buf[0x19] = 0x0; buf[0x1a] = 0x0; - buf[0x1b] = enable_dualsense ? 0x0 : 0x1; + + // 1 is classic DualShock, 0 is DualSense, but using 0 requires setting [0x19] and [0x1a] to + // values taken from raw HID, which is generally not available. But setting 1 for both seems + // to always work fine. + buf[0x1b] = 0x1; } CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state) diff --git a/lib/src/takion.c b/lib/src/takion.c index f786d6f..baae28c 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -558,7 +558,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_state(ChiakiTakion *ta else { buf_sz = 0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12; - chiaki_feedback_state_format_v12(buf + 0xc, feedback_state, takion->enable_dualsense); + chiaki_feedback_state_format_v12(buf + 0xc, feedback_state); } return takion_send_feedback_packet(takion, buf, buf_sz); } From c2f09326702a723c3365c49a25d30047beecb154 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Tue, 1 Nov 2022 10:37:20 +0100 Subject: [PATCH 092/104] gui: Support for DualSense haptics and trigger effects Haptics with PulseAudio does not seem to be working properly, so using Pipewire as a backend is recommended (and picked by default, if available via an SDL hint). --- .appveyor.yml | 2 +- gui/CMakeLists.txt | 2 +- gui/include/controllermanager.h | 35 ++++++ gui/include/settings.h | 3 + gui/include/settingsdialog.h | 2 + gui/include/streamsession.h | 7 ++ gui/src/controllermanager.cpp | 52 ++++++++- gui/src/settingsdialog.cpp | 10 ++ gui/src/streamsession.cpp | 167 ++++++++++++++++++++++++++++- scripts/appveyor-win.sh | 10 +- scripts/build-sdl2.sh | 4 +- scripts/kitware-archive-latest.asc | 120 ++++++++++----------- 12 files changed, 343 insertions(+), 71 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 7dc4799..db3c3a0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -37,7 +37,7 @@ for: install: - git submodule update --init --recursive - sudo pip3 install protobuf - - brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf + - HOMEBREW_NO_AUTO_UPDATE=1 brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf - scripts/build-ffmpeg.sh build_script: diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 5e51b55..443150f 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -67,12 +67,12 @@ if(CHIAKI_ENABLE_CLI) endif() target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg) +target_link_libraries(chiaki SDL2::SDL2) if(APPLE) target_link_libraries(chiaki Qt5::MacExtras) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS) endif() if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) - target_link_libraries(chiaki SDL2::SDL2) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) endif() if(CHIAKI_ENABLE_SETSU) diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 9e89fcc..eee0120 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -77,6 +77,7 @@ class Controller : public QObject int id; ChiakiOrientationTracker orientation_tracker; ChiakiControllerState state; + bool is_dualsense; #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER QMap, uint8_t> touch_ids; @@ -91,9 +92,43 @@ class Controller : public QObject QString GetName(); ChiakiControllerState GetState(); void SetRumble(uint8_t left, uint8_t right); + void SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right); + bool IsDualSense(); signals: void StateChanged(); }; +/* PS5 trigger effect documentation: + https://controllers.fandom.com/wiki/Sony_DualSense#FFB_Trigger_Modes + + Taken from SDL2, licensed under the zlib license, + Copyright (C) 1997-2022 Sam Lantinga + https://github.com/libsdl-org/SDL/blob/release-2.24.1/test/testgamecontroller.c#L263-L289 +*/ +typedef struct +{ + Uint8 ucEnableBits1; /* 0 */ + Uint8 ucEnableBits2; /* 1 */ + Uint8 ucRumbleRight; /* 2 */ + Uint8 ucRumbleLeft; /* 3 */ + Uint8 ucHeadphoneVolume; /* 4 */ + Uint8 ucSpeakerVolume; /* 5 */ + Uint8 ucMicrophoneVolume; /* 6 */ + Uint8 ucAudioEnableBits; /* 7 */ + Uint8 ucMicLightMode; /* 8 */ + Uint8 ucAudioMuteBits; /* 9 */ + Uint8 rgucRightTriggerEffect[11]; /* 10 */ + Uint8 rgucLeftTriggerEffect[11]; /* 21 */ + Uint8 rgucUnknown1[6]; /* 32 */ + Uint8 ucLedFlags; /* 38 */ + Uint8 rgucUnknown2[2]; /* 39 */ + Uint8 ucLedAnim; /* 41 */ + Uint8 ucLedBrightness; /* 42 */ + Uint8 ucPadLights; /* 43 */ + Uint8 ucLedRed; /* 44 */ + Uint8 ucLedGreen; /* 45 */ + Uint8 ucLedBlue; /* 46 */ +} DS5EffectsState_t; + #endif // CHIAKI_CONTROLLERMANAGER_H diff --git a/gui/include/settings.h b/gui/include/settings.h index c2320b8..00f38ab 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -63,6 +63,9 @@ class Settings : public QObject void SetLogVerbose(bool enabled) { settings.setValue("settings/log_verbose", enabled); } uint32_t GetLogLevelMask(); + bool GetDualSenseEnabled() const { return settings.value("settings/dualsense_enabled", false).toBool(); } + void SetDualSenseEnabled(bool enabled) { settings.setValue("settings/dualsense_enabled", enabled); } + ChiakiVideoResolutionPreset GetResolution() const; void SetResolution(ChiakiVideoResolutionPreset resolution); diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index d564bef..00af648 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -20,6 +20,7 @@ class SettingsDialog : public QDialog QCheckBox *log_verbose_check_box; QComboBox *disconnect_action_combo_box; + QCheckBox *dualsense_check_box; QComboBox *resolution_combo_box; QComboBox *fps_combo_box; @@ -37,6 +38,7 @@ class SettingsDialog : public QDialog private slots: void LogVerboseChanged(); + void DualSenseChanged(); void DisconnectActionSelected(); void ResolutionSelected(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index e139f45..4b0ba01 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -56,6 +56,7 @@ struct StreamSessionConnectInfo bool fullscreen; TransformMode transform_mode; bool enable_keyboard; + bool enable_dualsense; StreamSessionConnectInfo( Settings *settings, @@ -101,17 +102,23 @@ class StreamSession : public QObject unsigned int audio_buffer_size; QAudioOutput *audio_output; QIODevice *audio_io; + SDL_AudioDeviceID haptics_output; + uint8_t *haptics_resampler_buf; QMap key_map; void PushAudioFrame(int16_t *buf, size_t samples_count); + void PushHapticsFrame(uint8_t *buf, size_t buf_size); #if CHIAKI_GUI_ENABLE_SETSU void HandleSetsuEvent(SetsuEvent *event); #endif private slots: void InitAudio(unsigned int channels, unsigned int rate); + void InitHaptics(); void Event(ChiakiEvent *event); + void DisconnectHaptics(); + void ConnectHaptics(); public: explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr); diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index b1f38e7..8cc501f 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -68,6 +68,12 @@ static QSet chiaki_motion_controller_guids({ "030000008f0e00001431000000000000", }); +static QSet> chiaki_dualsense_controller_ids({ + // in format (vendor id, product id) + QPair(0x054c, 0x0ce6), // DualSense controller + QPair(0x054c, 0x0df2), // DualSense Edge controller +}); + static ControllerManager *instance = nullptr; #define UPDATE_INTERVAL_MS 4 @@ -84,6 +90,15 @@ ControllerManager::ControllerManager(QObject *parent) { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER SDL_SetMainReady(); +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); +#endif +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); +#endif +#ifdef SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); +#endif if(SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) { const char *err = SDL_GetError(); @@ -225,7 +240,8 @@ void ControllerManager::ControllerClosed(Controller *controller) open_controllers.remove(controller->GetDeviceID()); } -Controller::Controller(int device_id, ControllerManager *manager) : QObject(manager) +Controller::Controller(int device_id, ControllerManager *manager) + : QObject(manager), is_dualsense(false) { this->id = device_id; this->manager = manager; @@ -244,8 +260,10 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO)) SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); - break; #endif + auto controller_id = QPair(SDL_GameControllerGetVendor(controller), SDL_GameControllerGetProduct(controller)); + is_dualsense = chiaki_dualsense_controller_ids.contains(controller_id); + break; } } #endif @@ -255,7 +273,12 @@ Controller::~Controller() { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(controller) + { + // Clear trigger effects, SDL doesn't do it automatically + const uint8_t clear_effect[10] = { 0 }; + this->SetTriggerEffects(0x05, clear_effect, 0x05, clear_effect); SDL_GameControllerClose(controller); + } #endif manager->ControllerClosed(this); } @@ -487,3 +510,28 @@ void Controller::SetRumble(uint8_t left, uint8_t right) SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000); #endif } + +void Controller::SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right) +{ +#if defined(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) && SDL_VERSION_ATLEAST(2, 0, 16) + if(!is_dualsense || !controller) + return; + DS5EffectsState_t state; + SDL_zero(state); + state.ucEnableBits1 |= (0x04 /* left trigger */ | 0x08 /* right trigger */); + state.rgucLeftTriggerEffect[0] = type_left; + SDL_memcpy(state.rgucLeftTriggerEffect + 1, data_left, 10); + state.rgucRightTriggerEffect[0] = type_right; + SDL_memcpy(state.rgucRightTriggerEffect + 1, data_right, 10); + SDL_GameControllerSendEffect(controller, &state, sizeof(state)); +#endif +} + +bool Controller::IsDualSense() +{ +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + if(!controller) + return false; + return is_dualsense; +#endif +} diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 3f8856f..1770932 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -69,6 +69,11 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa log_verbose_check_box->setChecked(settings->GetLogVerbose()); connect(log_verbose_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::LogVerboseChanged); + dualsense_check_box = new QCheckBox(this); + general_layout->addRow(tr("Extended DualSense Support:\nEnable haptics and adaptive triggers\nfor attached DualSense controllers.\nThis is currently experimental."), dualsense_check_box); + dualsense_check_box->setChecked(settings->GetDualSenseEnabled()); + connect(dualsense_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::DualSenseChanged); + auto log_directory_label = new QLineEdit(GetLogBaseDir(), this); log_directory_label->setReadOnly(true); general_layout->addRow(tr("Log Directory:"), log_directory_label); @@ -322,6 +327,11 @@ void SettingsDialog::LogVerboseChanged() settings->SetLogVerbose(log_verbose_check_box->isChecked()); } +void SettingsDialog::DualSenseChanged() +{ + settings->SetDualSenseEnabled(dualsense_check_box->isChecked()); +} + void SettingsDialog::FPSSelected() { settings->SetFPS((ChiakiVideoFPSPreset)fps_combo_box->currentData().toInt()); diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index f5b7ace..42c9959 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -14,6 +14,12 @@ #define SETSU_UPDATE_INTERVAL_MS 4 +#ifdef Q_OS_LINUX +#define DUALSENSE_AUDIO_DEVICE_NEEDLE "DualSense" +#else +#define DUALSENSE_AUDIO_DEVICE_NEEDLE "Wireless Controller" +#endif + StreamSessionConnectInfo::StreamSessionConnectInfo( Settings *settings, ChiakiTarget target, @@ -39,10 +45,12 @@ StreamSessionConnectInfo::StreamSessionConnectInfo( this->fullscreen = fullscreen; this->transform_mode = transform_mode; this->enable_keyboard = false; // TODO: from settings + this->enable_dualsense = settings->GetDualSenseEnabled(); } static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user); +static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user); static void EventCb(ChiakiEvent *event, void *user); #if CHIAKI_GUI_ENABLE_SETSU static void SessionSetsuCb(SetsuEvent *event, void *user); @@ -57,7 +65,9 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje pi_decoder(nullptr), #endif audio_output(nullptr), - audio_io(nullptr) + audio_io(nullptr), + haptics_output(0), + haptics_resampler_buf(nullptr) { connected = false; ChiakiErrorCode err; @@ -116,6 +126,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_connect_info.video_profile = connect_info.video_profile; chiaki_connect_info.video_profile_auto_downgrade = true; chiaki_connect_info.enable_keyboard = false; + chiaki_connect_info.enable_dualsense = connect_info.enable_dualsense; #if CHIAKI_LIB_ENABLE_PI_DECODER if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264) @@ -144,6 +155,14 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_opus_decoder_get_sink(&opus_decoder, &audio_sink); chiaki_session_set_audio_sink(&session, &audio_sink); + if(connect_info.enable_dualsense) + { + ChiakiAudioSink haptics_sink; + haptics_sink.user = this; + haptics_sink.frame_cb = HapticsFrameCb; + chiaki_session_set_haptics_sink(&session, &haptics_sink); + } + #if CHIAKI_LIB_ENABLE_PI_DECODER if(pi_decoder) chiaki_session_set_video_sample_cb(&session, chiaki_pi_decoder_video_sample_cb, pi_decoder); @@ -181,6 +200,10 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje #endif key_map = connect_info.key_map; + if(connect_info.enable_dualsense) + { + InitHaptics(); + } UpdateGamepads(); } @@ -208,6 +231,16 @@ StreamSession::~StreamSession() chiaki_ffmpeg_decoder_fini(ffmpeg_decoder); delete ffmpeg_decoder; } + if(haptics_output > 0) + { + SDL_CloseAudioDevice(haptics_output); + haptics_output = 0; + } + if(haptics_resampler_buf) + { + free(haptics_resampler_buf); + haptics_resampler_buf = nullptr; + } } void StreamSession::Start() @@ -312,6 +345,8 @@ void StreamSession::UpdateGamepads() { CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID()); controllers.remove(controller_id); + if(controller->IsDualSense()) + DisconnectHaptics(); delete controller; } } @@ -330,6 +365,11 @@ void StreamSession::UpdateGamepads() CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData()); connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState); controllers[controller_id] = controller; + if(controller->IsDualSense()) + { + // Connect haptics audio device with a delay to give the sound system time to set up + QTimer::singleShot(1000, this, &StreamSession::ConnectHaptics); + } } } @@ -388,6 +428,82 @@ void StreamSession::InitAudio(unsigned int channels, unsigned int rate) channels, rate, audio_output->bufferSize()); } +void StreamSession::InitHaptics() +{ + haptics_output = 0; + haptics_resampler_buf = nullptr; +#ifdef Q_OS_LINUX + // Haptics work most reliably with Pipewire, so try to use that if available + SDL_SetHint("SDL_AUDIODRIVER", "pipewire"); +#endif + + if(SDL_Init(SDL_INIT_AUDIO) < 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Could not initialize SDL Audio for haptics output: %s", SDL_GetError()); + return; + } + +#ifdef Q_OS_LINUX + if(!strstr(SDL_GetCurrentAudioDriver(), "pipewire")) + { + CHIAKI_LOGW( + log.GetChiakiLog(), + "Haptics output is not using Pipewire, this may not work reliably. (was: '%s')", + SDL_GetCurrentAudioDriver()); + } +#endif + + SDL_AudioCVT cvt; + SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000); + cvt.len = 240; // 10 16bit stereo samples + haptics_resampler_buf = (uint8_t*) calloc(cvt.len * cvt.len_mult, sizeof(uint8_t)); +} + +void StreamSession::DisconnectHaptics() +{ + if(this->haptics_output > 0) + { + SDL_CloseAudioDevice(haptics_output); + this->haptics_output = 0; + } +} + +void StreamSession::ConnectHaptics() +{ + if(this->haptics_output > 0) + { + CHIAKI_LOGW(this->log.GetChiakiLog(), "Haptics already connected to an attached DualSense controller, ignoring additional controllers."); + return; + } + + SDL_AudioSpec want, have; + SDL_zero(want); + want.freq = 48000; + want.format = AUDIO_S16LSB; + want.channels = 4; + want.samples = 480; // 10ms buffer + want.callback = NULL; + + const char *device_name = nullptr; + for(int i=0; i < SDL_GetNumAudioDevices(0); i++) + { + device_name = SDL_GetAudioDeviceName(i, 0); + if(!device_name || !strstr(device_name, DUALSENSE_AUDIO_DEVICE_NEEDLE)) + continue; + haptics_output = SDL_OpenAudioDevice(device_name, 0, &want, &have, 0); + if(haptics_output == 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Could not open SDL Audio Device %s for haptics output: %s", device_name, SDL_GetError()); + continue; + } + SDL_PauseAudioDevice(haptics_output, 0); + CHIAKI_LOGI(log.GetChiakiLog(), "Haptics Audio Device '%s' opened with %d channels @ %d Hz, buffer size %u (driver=%s)", device_name, have.channels, have.freq, have.size, SDL_GetCurrentAudioDriver()); + return; + } + CHIAKI_LOGW(log.GetChiakiLog(), "DualSense features were enabled and a DualSense is connected, but could not find the DualSense audio device!"); + return; +} + void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) { if(!audio_io) @@ -395,6 +511,35 @@ void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) audio_io->write((const char *)buf, static_cast(samples_count * 2 * 2)); } +void StreamSession::PushHapticsFrame(uint8_t *buf, size_t buf_size) +{ + if(haptics_output == 0) + return; + SDL_AudioCVT cvt; + // Haptics samples are coming in at 3KHZ, but the DualSense expects 48KHZ + SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000); + cvt.len = buf_size * 2; + cvt.buf = haptics_resampler_buf; + // Remix to 4 channels + for (int i=0; i < buf_size; i+=4) + { + SDL_memset(haptics_resampler_buf + i * 2, 0, 4); + SDL_memcpy(haptics_resampler_buf + (i * 2) + 4, buf + i, 4); + } + // Resample to 48kHZ + if(SDL_ConvertAudio(&cvt) != 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Failed to resample haptics audio: %s", SDL_GetError()); + return; + } + + if(SDL_QueueAudio(haptics_output, cvt.buf, cvt.len_cvt) < 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Failed to submit haptics audio to device: %s", SDL_GetError()); + return; + } +} + void StreamSession::Event(ChiakiEvent *event) { switch(event->type) @@ -418,6 +563,19 @@ void StreamSession::Event(ChiakiEvent *event) }); break; } + case CHIAKI_EVENT_TRIGGER_EFFECTS: { + uint8_t type_left = event->trigger_effects.type_left; + uint8_t data_left[10]; + memcpy(data_left, event->trigger_effects.left, 10); + uint8_t data_right[10]; + memcpy(data_right, event->trigger_effects.right, 10); + uint8_t type_right = event->trigger_effects.type_right; + QMetaObject::invokeMethod(this, [this, type_left, data_left, type_right, data_right]() { + for(auto controller : controllers) + controller->SetTriggerEffects(type_left, data_left, type_right, data_right); + }); + break; + } default: break; } @@ -544,6 +702,7 @@ class StreamSessionPrivate } static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); } + static void PushHapticsFrame(StreamSession *session, uint8_t *buf, size_t buf_size) { session->PushHapticsFrame(buf, buf_size); } static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); } #if CHIAKI_GUI_ENABLE_SETSU static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); } @@ -563,6 +722,12 @@ static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user) StreamSessionPrivate::PushAudioFrame(session, buf, samples_count); } +static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user) +{ + auto session = reinterpret_cast(user); + StreamSessionPrivate::PushHapticsFrame(session, buf, buf_size); +} + static void EventCb(ChiakiEvent *event, void *user) { auto session = reinterpret_cast(user); diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index a7e63a0..7bbc0f3 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -30,14 +30,15 @@ ninja ninja install cd ../.. -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip +wget https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1s.zip && 7z x openssl-1.1.*.zip -wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip -export SDL_ROOT="$BUILD_ROOT/SDL2-2.0.10" +wget https://www.libsdl.org/release/SDL2-devel-2.26.2-VC.zip && 7z x SDL2-devel-2.26.2-VC.zip +export SDL_ROOT="$BUILD_ROOT/SDL2-2.26.2" export SDL_ROOT=${SDL_ROOT//[\\]//} echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\") set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\") -set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake" +set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\") +include($SDL_ROOT/cmake/sdl2-config-version.cmake)" > "$SDL_ROOT/SDL2Config.cmake" mkdir protoc && cd protoc wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip @@ -55,6 +56,7 @@ echo "-- Configure" mkdir build && cd build + cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ diff --git a/scripts/build-sdl2.sh b/scripts/build-sdl2.sh index 725bbf4..716b486 100755 --- a/scripts/build-sdl2.sh +++ b/scripts/build-sdl2.sh @@ -22,14 +22,14 @@ mkdir -p build && cd build || exit 1 cmake \ -DCMAKE_INSTALL_PREFIX="$ROOT/sdl2-prefix" \ -DSDL_ATOMIC=OFF \ - -DSDL_AUDIO=OFF \ + -DSDL_AUDIO=ON \ -DSDL_CPUINFO=OFF \ -DSDL_EVENTS=ON \ -DSDL_FILE=OFF \ -DSDL_FILESYSTEM=OFF \ -DSDL_HAPTIC=ON \ -DSDL_JOYSTICK=ON \ - -DSDL_LOADSO=OFF \ + -DSDL_LOADSO=ON \ -DSDL_RENDER=OFF \ -DSDL_SHARED=ON \ -DSDL_STATIC=OFF \ diff --git a/scripts/kitware-archive-latest.asc b/scripts/kitware-archive-latest.asc index 6b3a357..2c95d3e 100644 --- a/scripts/kitware-archive-latest.asc +++ b/scripts/kitware-archive-latest.asc @@ -1,64 +1,64 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -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 -=cUKW +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 +=8BHh -----END PGP PUBLIC KEY BLOCK----- From e14083c87c10d734e0cdecd133d8a9e0d6bd00e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 5 Feb 2023 17:05:06 +0100 Subject: [PATCH 093/104] Update android target SDK to 33 and dependencies --- .builds/android.yml | 2 +- android/app/build.gradle | 29 +++++++++---------- android/app/src/main/AndroidManifest.xml | 4 ++- android/app/src/main/cpp/oboe | 2 +- .../com/metallic/chiaki/common/ManualHost.kt | 2 +- .../metallic/chiaki/common/RegisteredHost.kt | 2 +- .../chiaki/common/SerializedSettings.kt | 4 ++- .../metallic/chiaki/stream/StreamActivity.kt | 6 ++-- android/build.gradle | 4 +-- .../gradle/wrapper/gradle-wrapper.properties | 6 ++-- cmake/OpenSSLExternalProject.cmake | 4 +-- 11 files changed, 35 insertions(+), 30 deletions(-) diff --git a/.builds/android.yml b/.builds/android.yml index 5e4c259..20cd88b 100644 --- a/.builds/android.yml +++ b/.builds/android.yml @@ -22,7 +22,7 @@ tasks: sudo docker run \ -v /home/build:/home/build \ -u $(id -u):$(id -g) \ - thestr4ng3r/android:b2853cc \ + thestr4ng3r/android:90d826e \ /bin/bash -c "cd /home/build/chiaki/android && ./gradlew assembleRelease bundleRelease" cp chiaki/android/app/build/outputs/apk/release/app-release*.apk Chiaki.apk cp chiaki/android/app/build/outputs/bundle/release/app-release*.aab Chiaki.aab diff --git a/android/app/build.gradle b/android/app/build.gradle index 87b8761..99194ee 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -18,12 +18,11 @@ def chiakiVersion = "$chiakiVersionMajor.$chiakiVersionMinor.$chiakiVersionPatch println("Determined Chiaki Version: $chiakiVersion") android { - compileSdkVersion 30 - buildToolsVersion "30.0.2" + compileSdkVersion 33 defaultConfig { applicationId "com.metallic.chiaki" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 11 versionName chiakiVersion externalNativeBuild { @@ -52,7 +51,7 @@ android { } externalNativeBuild { cmake { - version "3.10.2+" + version "3.22.1" path rootCMakeLists } } @@ -95,23 +94,23 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.preference:preference:1.1.1' - implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.appcompat:appcompat:1.6.0' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.preference:preference:1.2.0' + implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' - implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.2.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.5.1' implementation "io.reactivex.rxjava2:rxjava:2.2.20" implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - def room_version = "2.2.6" + def room_version = "2.5.0" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-rxjava2:$room_version" - implementation "com.squareup.moshi:moshi:1.9.2" - kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2" + implementation "com.squareup.moshi:moshi:1.14.0" + kapt "com.squareup.moshi:moshi-kotlin-codegen:1.14.0" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fa14662..a0d28cb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + android:configChanges="keyboard|keyboardHidden|orientation|screenSize" + android:exported="true"> diff --git a/android/app/src/main/cpp/oboe b/android/app/src/main/cpp/oboe index 0ab5b12..8740d0f 160000 --- a/android/app/src/main/cpp/oboe +++ b/android/app/src/main/cpp/oboe @@ -1 +1 @@ -Subproject commit 0ab5b12a5bc3630a3d6c83b20eed2a669ebf7a24 +Subproject commit 8740d0fc321a55489dbbf6067298201b7d2e106d diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt index 5825000..7065610 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt @@ -3,7 +3,7 @@ package com.metallic.chiaki.common import androidx.room.* -import androidx.room.ForeignKey.SET_NULL +import androidx.room.ForeignKey.Companion.SET_NULL import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Single diff --git a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt index b96d88d..d920a8c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt @@ -3,7 +3,7 @@ package com.metallic.chiaki.common import androidx.room.* -import androidx.room.ColumnInfo.BLOB +import androidx.room.ColumnInfo.Companion.BLOB import com.metallic.chiaki.lib.RegistHost import com.metallic.chiaki.lib.Target import io.reactivex.Completable diff --git a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt index 8334dc5..74257ac 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt @@ -25,6 +25,8 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.schedulers.Schedulers import okio.Buffer import okio.Okio +import okio.buffer +import okio.source import java.io.File import java.io.IOException @@ -164,7 +166,7 @@ fun importSettingsFromUri(activity: Activity, uri: Uri, disposable: CompositeDis try { val inputStream = activity.contentResolver.openInputStream(uri) ?: throw IOException() - val buffer = Okio.buffer(Okio.source(inputStream)) + val buffer = inputStream.source().buffer() val reader = JsonReader.of(buffer) val adapter = moshi().serializedSettingsAdapter() diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index b0248be..a332961 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -174,7 +174,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe .alpha(1.0f) .setListener(object: AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) + override fun onAnimationEnd(animation: Animator) { binding.overlay.alpha = 1.0f } @@ -189,7 +189,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe .alpha(0.0f) .setListener(object: AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) + override fun onAnimationEnd(animation: Animator) { binding.overlay.isGone = true } @@ -306,6 +306,8 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe dialog.show() } } + + else -> {} } } diff --git a/android/build.gradle b/android/build.gradle index 7ec4859..cdfc5d6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.21' + ext.kotlin_version = '1.8.0' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c21407a..7a0d628 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jan 15 11:37:05 CET 2021 +#Sun Feb 05 16:25:19 CET 2023 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/cmake/OpenSSLExternalProject.cmake b/cmake/OpenSSLExternalProject.cmake index 0538d48..1e1370d 100644 --- a/cmake/OpenSSLExternalProject.cmake +++ b/cmake/OpenSSLExternalProject.cmake @@ -33,8 +33,8 @@ endif() find_program(MAKE_EXE NAMES gmake make) ExternalProject_Add(OpenSSL-ExternalProject - URL https://www.openssl.org/source/openssl-1.1.1d.tar.gz - URL_HASH SHA256=1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2 + URL https://www.openssl.org/source/openssl-1.1.1s.tar.gz + URL_HASH SHA256=c5ac01e760ee6ff0dab61d6b2bbd30146724d063eb322180c6f18a6f74e4b6aa INSTALL_DIR "${OPENSSL_INSTALL_DIR}" CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${OPENSSL_BUILD_ENV} "/Configure" "--prefix=" no-shared ${OPENSSL_CONFIG_EXTRA_ARGS} "${OPENSSL_OS_COMPILER}" From 582ec7aa5459b2916a430d6b761ca8c76da8e639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 8 Feb 2023 14:08:17 +0100 Subject: [PATCH 094/104] Allow specifying command in switch podman script --- scripts/switch/run-podman-build-chiaki.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/switch/run-podman-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh index f9e05c0..0e8dede 100755 --- a/scripts/switch/run-podman-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -7,5 +7,4 @@ podman run --rm \ -w "/build/chiaki" \ -it \ thestr4ng3r/chiaki-build-switch:v2 \ - /bin/bash -c "scripts/switch/build.sh" - + ${1:-/bin/bash -c "scripts/switch/build.sh"} From 6096de8c13881062bc40cf1801afdf3e3d6ce1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 8 Feb 2023 14:08:45 +0100 Subject: [PATCH 095/104] Do not handle server shutdown as error When the console is powered off or put into sleep mode during streaming, the remote will disconnect and report the string "Server shutting down". This is expected by the user and thus should not be shown as an error message. --- android/app/src/main/cpp/chiaki-jni.c | 4 +- .../java/com/metallic/chiaki/lib/Chiaki.kt | 7 +-- .../metallic/chiaki/stream/StreamActivity.kt | 47 ++++++++++--------- gui/src/streamwindow.cpp | 2 +- lib/include/chiaki/session.h | 8 +++- lib/src/session.c | 7 ++- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index d0b374a..9b14b60 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -110,9 +110,9 @@ JNIEXPORT jstring JNICALL JNI_FCN(quitReasonToString)(JNIEnv *env, jobject obj, return E->NewStringUTF(env, chiaki_quit_reason_string((ChiakiQuitReason)value)); } -JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsStopped)(JNIEnv *env, jobject obj, jint value) +JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsError)(JNIEnv *env, jobject obj, jint value) { - return value == CHIAKI_QUIT_REASON_STOPPED; + return chiaki_quit_reason_is_error(value); } JNIEXPORT jobject JNICALL JNI_FCN(videoProfilePreset)(JNIEnv *env, jobject obj, jint resolution_preset, jint fps_preset, jobject codec) diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 2c474f0..bb0b0a6 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -83,7 +83,7 @@ private class ChiakiNative } @JvmStatic external fun errorCodeToString(value: Int): String @JvmStatic external fun quitReasonToString(value: Int): String - @JvmStatic external fun quitReasonIsStopped(value: Int): Boolean + @JvmStatic external fun quitReasonIsError(value: Int): Boolean @JvmStatic external fun videoProfilePreset(resolutionPreset: Int, fpsPreset: Int, codec: Codec): ConnectVideoProfile @JvmStatic external fun sessionCreate(result: CreateResult, connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean, javaSession: Session) @JvmStatic external fun sessionFree(ptr: Long) @@ -309,10 +309,7 @@ class QuitReason(val value: Int) { override fun toString() = ChiakiNative.quitReasonToString(value) - /** - * whether the reason is CHIAKI_QUIT_REASON_STOPPED - */ - val isStopped = ChiakiNative.quitReasonIsStopped(value) + val isError = ChiakiNative.quitReasonIsError(value) } sealed class Event diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index a332961..bd77e0c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -230,28 +230,33 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe { is StreamStateQuit -> { - if(!state.reason.isStopped && dialogContents != StreamQuitDialog) + if(dialogContents != StreamQuitDialog) { - dialog?.dismiss() - val reasonStr = state.reasonString - val dialog = MaterialAlertDialogBuilder(this) - .setMessage(getString(R.string.alert_message_session_quit, state.reason.toString()) - + (if(reasonStr != null) "\n$reasonStr" else "")) - .setPositiveButton(R.string.action_reconnect) { _, _ -> - dialog = null - reconnect() - } - .setOnCancelListener { - dialog = null - finish() - } - .setNegativeButton(R.string.action_quit_session) { _, _ -> - dialog = null - finish() - } - .create() - dialogContents = StreamQuitDialog - dialog.show() + if(state.reason.isError) + { + dialog?.dismiss() + val reasonStr = state.reasonString + val dialog = MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.alert_message_session_quit, state.reason.toString()) + + (if(reasonStr != null) "\n$reasonStr" else "")) + .setPositiveButton(R.string.action_reconnect) { _, _ -> + dialog = null + reconnect() + } + .setOnCancelListener { + dialog = null + finish() + } + .setNegativeButton(R.string.action_quit_session) { _, _ -> + dialog = null + finish() + } + .create() + dialogContents = StreamQuitDialog + dialog.show() + } + else + finish() } } diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index d1c0b95..a2337d7 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -201,7 +201,7 @@ void StreamWindow::closeEvent(QCloseEvent *event) void StreamWindow::SessionQuit(ChiakiQuitReason reason, const QString &reason_str) { - if(reason != CHIAKI_QUIT_REASON_STOPPED) + if(chiaki_quit_reason_is_error(reason)) { QString m = tr("Chiaki Session has quit") + ":\n" + chiaki_quit_reason_string(reason); if(!reason_str.isEmpty()) diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 4f9f932..5c355ad 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -94,11 +94,17 @@ typedef enum { CHIAKI_QUIT_REASON_CTRL_CONNECT_FAILED, CHIAKI_QUIT_REASON_CTRL_CONNECTION_REFUSED, CHIAKI_QUIT_REASON_STREAM_CONNECTION_UNKNOWN, - CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED + CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED, + CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN, // like REMOTE_DISCONNECTED, but because the server shut down } ChiakiQuitReason; CHIAKI_EXPORT const char *chiaki_quit_reason_string(ChiakiQuitReason reason); +static inline bool chiaki_quit_reason_is_error(ChiakiQuitReason reason) +{ + return reason != CHIAKI_QUIT_REASON_STOPPED && reason != CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN; +} + typedef struct chiaki_quit_event_t { ChiakiQuitReason reason; diff --git a/lib/src/session.c b/lib/src/session.c index ea8e09d..aa56e19 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -158,6 +158,8 @@ CHIAKI_EXPORT const char *chiaki_quit_reason_string(ChiakiQuitReason reason) return "Unknown Error in Stream Connection"; case CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED: return "Remote has disconnected from Stream Connection"; + case CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN: + return "Remote has disconnected from Stream Connection the because Server shut down"; case CHIAKI_QUIT_REASON_NONE: default: return "Unknown"; @@ -505,7 +507,10 @@ ctrl_failed: if(err == CHIAKI_ERR_DISCONNECTED) { CHIAKI_LOGE(session->log, "Remote disconnected from StreamConnection"); - session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED; + if(!strcmp(session->stream_connection.remote_disconnect_reason, "Server shutting down")) + session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN; + else + session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED; session->quit_reason_str = strdup(session->stream_connection.remote_disconnect_reason); } else if(err != CHIAKI_ERR_SUCCESS && err != CHIAKI_ERR_CANCELED) From bcdd0dd7fd9fb6b83f830fafe3337ff14f200c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 19 Aug 2023 17:00:54 +0200 Subject: [PATCH 096/104] Update CI images --- .builds/common.yml | 2 +- .builds/openbsd.yml | 2 +- scripts/build-appimage.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.builds/common.yml b/.builds/common.yml index 0bce7cb..b692b08 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -1,5 +1,5 @@ -image: alpine/edge # on edge for https://gitlab.alpinelinux.org/alpine/aports/-/issues/13287 +image: alpine/latest sources: - https://git.sr.ht/~thestr4ng3r/chiaki diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index 2df5353..2f513c0 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,5 +1,5 @@ -image: openbsd/7.0 +image: openbsd/latest sources: - https://git.sr.ht/~thestr4ng3r/chiaki diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 4fbd588..ab025af 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -48,4 +48,4 @@ export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" export EXTRA_QT_PLUGINS=opengl ./linuxdeploy-x86_64.AppImage --appdir="${appdir}" -e "${appdir}/usr/bin/chiaki" -d "${appdir}/usr/share/applications/chiaki.desktop" --plugin qt --output appimage -mv Chiaki-*-x86_64.AppImage Chiaki.AppImage +mv Chiaki*-x86_64.AppImage Chiaki.AppImage From 666238ba9fbce9cc79c9b03f1370907d44f838c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 11:12:11 +0200 Subject: [PATCH 097/104] Bump version to 2.2.0 --- CMakeLists.txt | 4 ++-- android/app/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3af7f5c..64caaee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,8 +32,8 @@ tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of s tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO) set(CHIAKI_VERSION_MAJOR 2) -set(CHIAKI_VERSION_MINOR 1) -set(CHIAKI_VERSION_PATCH 1) +set(CHIAKI_VERSION_MINOR 2) +set(CHIAKI_VERSION_PATCH 0) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/android/app/build.gradle b/android/app/build.gradle index 99194ee..21a89f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,7 +23,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 33 - versionCode 11 + versionCode 12 versionName chiakiVersion externalNativeBuild { cmake { From d4a0603bf20a007d57aef2e63a384f0eb4041759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 11:17:05 +0200 Subject: [PATCH 098/104] Fix switch host_addr regex for more arbitrary strings --- switch/include/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switch/include/settings.h b/switch/include/settings.h index fbf43b8..72fc491 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -49,7 +49,7 @@ class Settings // the goal is to read/write inernal flat configuration file const std::map re_map = { {HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]")}, - {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?")}, + {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?([^\"]*)\"?")}, {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?([\\w_-]+)\"?")}, {PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?")}, From 89368f63c99d67cde8868c0269b66a1b0c507397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 11:21:25 +0200 Subject: [PATCH 099/104] Add script for local macOS distribution --- .gitignore | 2 ++ scripts/macos-dist-local.sh | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100755 scripts/macos-dist-local.sh diff --git a/.gitignore b/.gitignore index 0b9eccd..f30ef42 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ compile_commands.json chiaki.conf /appimage .cache/ +/*.app +/*.dmg diff --git a/scripts/macos-dist-local.sh b/scripts/macos-dist-local.sh new file mode 100755 index 0000000..d2ac740 --- /dev/null +++ b/scripts/macos-dist-local.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Build Chiaki for macOS distribution using dependencies from MacPorts and custom ffmpeg + +set -xe +cd $(dirname "${BASH_SOURCE[0]}")/.. +scripts/build-ffmpeg.sh +export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix" +scripts/build-common.sh +cp -a build/gui/chiaki.app Chiaki.app +/opt/local/libexec/qt5/bin/macdeployqt Chiaki.app + +# Remove all LC_RPATH load commands that have absolute paths of the build machine +RPATHS=$(otool -l Chiaki.app/Contents/MacOS/chiaki | grep -A 2 LC_RPATH | grep 'path /' | awk '{print $2}') +for p in ${RPATHS}; do install_name_tool -delete_rpath "$p" Chiaki.app/Contents/MacOS/chiaki; done + +# This may warn because we already ran macdeployqt above +/opt/local/libexec/qt5/bin/macdeployqt Chiaki.app -dmg From 8911a44766570c0d182c0e59f5eee5cfca548ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 12:53:52 +0200 Subject: [PATCH 100/104] Remove Play Store from README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5be248c..49ec331 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Builds are provided for Linux, Android, macOS, Nintendo Switch and Windows. You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). * **Linux**: The provided file is an [AppImage](https://appimage.org/). Simply make it executable (`chmod +x .AppImage`) and run it. -* **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. +* **Android**: Install from [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. * **Switch**: Download the `.nro` file and copy it into the `switch/` directory on your SD card. From 94fcdc3c6109cdcfbcf5524e5ab7838b230822b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 31 Jul 2024 15:52:37 +0200 Subject: [PATCH 101/104] Add reference to chiaki-ng --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 49ec331..1a94b66 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,17 @@ Chiaki is a Free and Open Source Software Client for PlayStation 4 and PlayStation 5 Remote Play for Linux, FreeBSD, OpenBSD, NetBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. -![Screenshot](assets/screenshot.png) - -## Project Status +## Project Status and Contributing As all relevant features are implemented, this project is considered to be finished and in maintenance mode only. -No major updates are planned and contributions are only accepted in special cases. +No major updates are planned and contributions are only accepted in special cases such as security issues. +The objective is to keep a stable base and not break existing support for less mainstream platforms such as BSDs. + +**For a more active, fast moving and community-oriented project, refer +to [chiaki-ng](https://streetpea.github.io/chiaki-ng/) ("next generation"). +If you would like to contribute, this will likely also be the best place to do so.** + +![Screenshot](assets/screenshot.png) ## Installing From 4eb90a7a658c93bca3b681b9d8e4282f21258b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 9 Jun 2025 12:20:27 +0200 Subject: [PATCH 102/104] Refresh switch to build again --- scripts/switch/dev-container.sh | 10 ++++++++++ scripts/switch/run-podman-build-chiaki.sh | 2 +- switch/CMakeLists.txt | 15 ++++++--------- switch/include/io.h | 2 +- switch/include/settings.h | 1 - switch/src/main.cpp | 2 -- switch/src/settings.cpp | 12 +++--------- 7 files changed, 21 insertions(+), 23 deletions(-) create mode 100755 scripts/switch/dev-container.sh diff --git a/scripts/switch/dev-container.sh b/scripts/switch/dev-container.sh new file mode 100755 index 0000000..4cfedb3 --- /dev/null +++ b/scripts/switch/dev-container.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd "`dirname $(readlink -f ${0})`/../.." + +podman run --rm \ + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + -it \ + quay.io/thestr4ng3r/chiaki-build-switch:v3 \ + /bin/bash diff --git a/scripts/switch/run-podman-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh index 0e8dede..513cdc8 100755 --- a/scripts/switch/run-podman-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -6,5 +6,5 @@ podman run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ -it \ - thestr4ng3r/chiaki-build-switch:v2 \ + quay.io/thestr4ng3r/chiaki-build-switch:v3 \ ${1:-/bin/bash -c "scripts/switch/build.sh"} diff --git a/switch/CMakeLists.txt b/switch/CMakeLists.txt index 4916a2e..3d5639a 100644 --- a/switch/CMakeLists.txt +++ b/switch/CMakeLists.txt @@ -61,17 +61,16 @@ target_include_directories(borealis PUBLIC find_package(glfw3 REQUIRED) find_library(EGL EGL) -find_library(GLAPI glapi) -find_library(DRM_NOUVEAU drm_nouveau) -target_link_libraries(borealis +target_link_libraries(borealis PUBLIC glfw - ${EGL} - ${GLAPI} - ${DRM_NOUVEAU}) + ${EGL}) if(CHIAKI_IS_SWITCH) target_compile_definitions(borealis PUBLIC BOREALIS_RESOURCES="romfs:/") + find_library(GLAPI glapi) + find_library(DRM_NOUVEAU drm_nouveau) + target_link_libraries(borealis PUBLIC ${GLAPI} ${DRM_NOUVEAU}) else() target_compile_definitions(borealis PUBLIC BOREALIS_RESOURCES="./switch/res/") @@ -114,9 +113,7 @@ target_link_libraries(chiaki-borealis if(CHIAKI_IS_SWITCH) # libnx is forced by the switch toolchain find_library(Z z) - find_library(GLAPI glapi) # TODO: make it transitive from borealis - find_library(DRM_NOUVEAU drm_nouveau) # TODO: make it transitive from borealis - target_link_libraries(chiaki-borealis ${Z} ${GLAPI} ${DRM_NOUVEAU}) + target_link_libraries(chiaki-borealis ${Z} ${GLAPI}) endif() install(TARGETS chiaki-borealis diff --git a/switch/include/io.h b/switch/include/io.h index 3c1031d..3bb6fd1 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -67,7 +67,7 @@ class IO // default nintendo switch res int screen_width = 1280; int screen_height = 720; - AVCodec *codec; + const AVCodec *codec; AVCodecContext *codec_context; AVFrame *frame; SDL_AudioDeviceID sdl_audio_device_id = 0; diff --git a/switch/include/settings.h b/switch/include/settings.h index 72fc491..6bcb445 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -61,7 +61,6 @@ class Settings }; ConfigurationItem ParseLine(std::string * line, std::string * value); - size_t GetB64encodeSize(size_t); public: // singleton configuration diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 6b7c95e..1dd33de 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -28,8 +28,6 @@ bool appletMainLoop() // use a custom nintendo switch socket config // chiaki requiers many threads with udp/tcp sockets static const SocketInitConfig g_chiakiSocketInitConfig = { - .bsdsockets_version = 1, - .tcp_tx_buf_size = 0x8000, .tcp_rx_buf_size = 0x10000, .tcp_tx_buf_max_size = 0x40000, diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index fb39861..7d3300a 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -29,11 +29,7 @@ Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string * return UNKNOWN; } -size_t Settings::GetB64encodeSize(size_t in) -{ - // calculate base64 buffer size after encode - return ((4 * in / 3) + 3) & ~3; -} +#define B64_ENCODED_SIZE(in) (((4 * in / 3) + 3) & ~3) Settings *Settings::instance = nullptr; @@ -458,8 +454,7 @@ std::string Settings::GetHostRPKey(Host *host) { if(host->rp_key_data || host->registered) { - size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); - char rp_key_b64[rp_key_b64_sz + 1] = { 0 }; + char rp_key_b64[B64_ENCODED_SIZE(0x10) + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( host->rp_key, 0x10, @@ -502,8 +497,7 @@ std::string Settings::GetHostRPRegistKey(Host *host) { if(host->rp_key_data || host->registered) { - size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); - char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = { 0 }; + char rp_regist_key_b64[B64_ENCODED_SIZE(CHIAKI_SESSION_AUTH_SIZE) + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( (uint8_t *)host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, From bb5a79f2349a96e390c2089d3ce9a053705f0b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 22 Jun 2025 11:57:41 +0200 Subject: [PATCH 103/104] Update dependencies in BSDs CI --- .builds/freebsd.yml | 5 +++-- .builds/openbsd.yml | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index c62a695..72109b5 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -1,5 +1,5 @@ -image: freebsd/13.x +image: freebsd/14.x sources: - https://git.sr.ht/~thestr4ng3r/chiaki @@ -7,7 +7,8 @@ sources: packages: - cmake - protobuf - - py39-protobuf + - py311-setuptools # should not be needed with nanopb >= 0.4.9 + - py311-protobuf - opus - qt5-core - qt5-qmake diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index 2f513c0..e1595ac 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -7,6 +7,7 @@ sources: packages: - cmake - protobuf + - py3-setuptools # should not be needed with nanopb >= 0.4.9 - py3-protobuf - opus - qtbase From a1fd41868588db0d90dc5ee6c2eac2bc46a62408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 25 Jun 2025 16:04:55 +0200 Subject: [PATCH 104/104] Update Ubuntu for AppImage to 24.04 --- scripts/Dockerfile.bionic | 16 ------- scripts/Dockerfile.noble | 13 ++++++ scripts/build-appimage.sh | 7 +-- scripts/build-ffmpeg.sh | 2 +- scripts/fetch-protoc.sh | 13 ------ scripts/kitware-archive-latest.asc | 64 ---------------------------- scripts/run-podman-build-appimage.sh | 5 ++- 7 files changed, 18 insertions(+), 102 deletions(-) delete mode 100644 scripts/Dockerfile.bionic create mode 100644 scripts/Dockerfile.noble delete mode 100755 scripts/fetch-protoc.sh delete mode 100644 scripts/kitware-archive-latest.asc diff --git a/scripts/Dockerfile.bionic b/scripts/Dockerfile.bionic deleted file mode 100644 index 2548a2d..0000000 --- a/scripts/Dockerfile.bionic +++ /dev/null @@ -1,16 +0,0 @@ - -FROM ubuntu:bionic - -RUN apt-get update -RUN apt-get install -y software-properties-common gpg wget -RUN add-apt-repository ppa:beineri/opt-qt-5.12.10-bionic -COPY kitware-archive-latest.asc /kitware-archive-latest.asc -RUN cat /kitware-archive-latest.asc | gpg --dearmor > /usr/share/keyrings/kitware-archive-keyring.gpg -RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' > /etc/apt/sources.list.d/kitware.list -RUN apt-get update -RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ - libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ - libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev - -CMD [] - diff --git a/scripts/Dockerfile.noble b/scripts/Dockerfile.noble new file mode 100644 index 0000000..abc33c3 --- /dev/null +++ b/scripts/Dockerfile.noble @@ -0,0 +1,13 @@ + +FROM ubuntu:noble + +RUN apt-get update +# Hint: python3-setuptools should not be needed with nanopb >= 0.4.9 +RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip \ + python3-protobuf protobuf-compiler \ + python3-setuptools \ + libssl-dev libopus-dev qtbase5-dev qtmultimedia5-dev libqt5multimedia5-plugins libqt5svg5-dev \ + libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev file + +CMD [] + diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index ab025af..05c0428 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -7,8 +7,6 @@ appdir=${1:-`pwd`/appimage/appdir} mkdir appimage -pip3 install --user protobuf==3.19.5 # need support for python 3.6 for running on bionic -scripts/fetch-protoc.sh appimage export PATH="`pwd`/appimage/protoc/bin:$PATH" scripts/build-ffmpeg.sh appimage scripts/build-sdl2.sh appimage @@ -18,7 +16,7 @@ cd build_appimage cmake \ -GNinja \ -DCMAKE_BUILD_TYPE=Release \ - "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix;/opt/qt512" \ + "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_ENABLE_GUI=ON \ @@ -40,9 +38,6 @@ curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuo chmod +x linuxdeploy-x86_64.AppImage curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage chmod +x linuxdeploy-plugin-qt-x86_64.AppImage -set +e -source /opt/qt512/bin/qt512-env.sh -set -e export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" export EXTRA_QT_PLUGINS=opengl diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index d00b962..9b6348f 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -5,7 +5,7 @@ cd "./$1" shift ROOT="`pwd`" -TAG=n4.3.1 +TAG=n4.3.9 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 diff --git a/scripts/fetch-protoc.sh b/scripts/fetch-protoc.sh deleted file mode 100755 index e1d2d2f..0000000 --- a/scripts/fetch-protoc.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -xe - -cd $(dirname "${BASH_SOURCE[0]}")/.. -cd "./$1" -ROOT="`pwd`" - -URL=https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-linux-x86_64.zip - -curl -L "$URL" -o protoc.zip -unzip protoc.zip -d protoc - diff --git a/scripts/kitware-archive-latest.asc b/scripts/kitware-archive-latest.asc deleted file mode 100644 index 2c95d3e..0000000 --- a/scripts/kitware-archive-latest.asc +++ /dev/null @@ -1,64 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBGK0u/sBEADD57vA+Zjb9sEUOM2HlwW8l0OJyxW/G4oxcTIaiC2Iuki5fXN1 -VgQD646hUmUh/eMxRcwMpUpihHLcmQxoFWMFwBmljB9Ext8vgthwJoOSr0UwjRTe -qt8IpgEk+2VTQ5/T2XSu//fhw28rP7k5+fMqdIC/COaM/+jCZC17trSkjFcPcPNY -jyC/p40iPfYPDzMdUZhCcxC4ovtlImI6Bkr0x1/NDdy1FsQ4mxFirvV2a0XgjizY -4r25CpgKkMolf9bjAT3Cx2RGYJ5etnB6Ck74NP0bKQikkeWLo2jmrnix+oU07p2Q -PgjsNw5soIczHwHm6mEtSN7vduqNa6QkGFFce0eDK2NIajxt620HUB53zrtaKj/J -ACwHj6SxCszbbHo83GiULGR+hmkPHnio5ob0gJwjMp6iWcbtgL4y19i+b8J696t+ -LL5IRBKqXM75XmHZW1munrAVeICWjSpQSYbIGEmYlcCtvxIl3VwH4KZUuhO3BAR6 -V5IgFjYIQkz/ngTywY/8KKaxMUWs2wE/lMLnJKPnDgGlvJ+JCPom0wjrdM2xm2WI -+PBAUXe8onw1hB/ozP6/pPN4p/H6+ZsiQ2razJcjgE9AYtGZY8tEB2fi7f5wNeg0 -irTmK363zMkqp7pfZMtARkogKBRzmR/8g+EHT4eFBwd3qm/g0Z98KcNaXwARAQAB -tEVLaXR3YXJlIEFwdCBBcmNoaXZlIEF1dG9tYXRpYyBTaWduaW5nIEtleSAoMjAy -MykgPGRlYmlhbkBraXR3YXJlLmNvbT6JAlQEEwEKAD4WIQQezC2Z2NabJNCvPiRA -PgvFpfV4KwUCYrS7+wIbAwUJBaOagAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAK -CRBAPgvFpfV4KyvuD/4n27+wbDt1sol4lyETR60CrVIZvklGscs5Sxawor7EsDYz -NMu4TVE4q/Wt6MwRmJd9OZBldxb8Wm8VxOEkq69X4aPeDhyPFTyJJNTsuIFlThPo -q2vkTno44EqQq37gcc20giCLgQL25CV92uvER8N9kBuT7JagDF8DE7ha0nsQ/zYp -3ikI1EQOIluYc2f3q02ZbLeRm7WqaVW+WDDfRH+6RCuACqeXhgmc8mSPc04nO0Kr -IS7M5/Eq8OOflQnQsV3DW8JUb9E9Q+P3+PMZ9mUbjjeNPZbD18dYWuHJObUWuP65 -g1w7jjAu/WWJwXQdBvxcDPmELLUZGnHJeVkrzuh9O1CBeuABltz8IvKb9MmC8Aob -q7996IljIMKfhBjae+/G2AuswgDykRJxKvTZYEIVngR8WaoZ7CAhBY7Ch+DElq/5 -AEz4QwVESJYitoyf3ei8nVZAHuoytgBgnUHh6bJSWXQZKAcm8w6b7cwTjyZxTg+a -sYXfgZq15BXec8McUp1hM0pA2GI1cMh+3zKz38kQnbUWT/Eo/en2JPWGlY018qxL -+Ok6wuZeUGH6ZB+DXJz1WukxVNupjd4OE3N4mdhZiemwSArVeK/P9yIFjyI0aiay -7IwL7LoKu1XfSYjUOODkr7e/CTOhITdZXCX0GdEt3WjOkCcwTfU3VJAh7zq8lbkC -DQRitLwFARAAwkCT3xuREKGfc33mxRwDZe+D4BuLSzOEx06/qSxccuuWa+HcMnDe -gG96kCFpRqzAdCiiN8b4u5A5tHO9lf+CUkykGI3afjTNbKnTKP+hN92CDfaJQ0bA -4n6I9yOAyJtwGSfX5ivTKEMdrPhw6TX2sASLuVBLbmPBtwRR/ciOdhRG2t51AXQl -Hp47dTTQP9Wo2HUFLmiqm/bYGXTnKMqP+hrfii868/arfNgO421/Up0s6vhuaDb9 -ZCq88GMe3v9gJ+yr/giTNyVKoK4yNEwzJIBJ0wlXULc4PQrX1QIR60gxfe8BJLcL -1wy+qGYhbdpHVn2k6dZsbJL3EmoetxmHJj5DmF7+KAmWhrUteF8Z5HtZyUKzLX9/ -IfkcVGSZdsJHb5gkBK0olSd6SXZF9r/ZpyHhc41gfABkGCXVfphSuFDlrutHcmwI -1RF69DQidl6ixP8O/dEN2iBgw2tvvMz/1dsN12U2LIpAhV2OL5ueIbgsmJE5w43g -K7Eb1D37KPbStWVyfZF2fOHN+Mg7Yjqun5Rq8pWKr+oxGYk2046DG4N+glCZrBKB -LnpNsMaLKFxhte5r1KrhyfRtYVQ10sWQB9wPPekRxqlDPog8KkX55WzxYPYH38Nd -qkgDdrILdxBkF3ThX5eEntQRarqVbVpLGk7eSYGPkpJ7l+RYiSehPSkAEQEAAYkE -cgQYAQoAJhYhBB7MLZnY1psk0K8+JEA+C8Wl9XgrBQJitLwFAhsCBQkFo5qAAkAJ -EEA+C8Wl9XgrwXQgBBkBCgAdFiEE9B5eruaZPk3sJUs1QtWhkrgZxdoFAmK0vAUA -CgkQQtWhkrgZxdpNvRAAvtCTTabvicfEXKgcv13FDWSE31F5hyAcK7spK7cehu3B -9+yU3nMJcPtIhA8VUQ/mC8sm5AGtWQetKn1nXRrS/xyssit8OSb8VPiOY7/HGD+R -9vSAgJwV+trr9inzz1ySmmEfuYi6xBK6YCO//lgtQZq8Ycd06yczSwgqyPOYiTfs -dG4wudOqob7Ea62p//kaOgv3HIWx3fuWa86Rfw56jsRzO9+lnUuDOXAfYcaev5af -BijcEySJfDgH2Lw1YfgOCu57VTJK8ZyTN29DWv7Ypjp0REOTdjFFf4gmpDX5Ib2D -lOUlDwu3ijmXGz63Pi8Col4UlE3i0vJk9WKcMTOIx/+e+83LGJjTwk2K4BWgzOl/ -ZRYGchf3dloQzdglumuhH3epyAZrjQXKCawUGn+7eKj+BqAVDtdOikdcZ2XtLO7F -YY5JMszPE3EwwngiYjJgo0YIOuj+JoEasLU1sVmIr1GYu0sySyknFuXooftnsLPY -hcw2gllLNK58XOARsuyWFa+b3NhmrJ+S4+hc0nMlEqsJPE8SksWjCTACtsK0CMnz -DbwDy1R5oV2+nzkRN1Up6341bbXLu6JrWVxegkrVa3mcDU3Z3/3Y4+vzn09lc2vz -wBpygs5ZvOWPPNamQf6QnlnWbjdDdjC+1qJZC5aM66vD7lbpVtDFIbzb+rGuCf3P -mw//angfnJh26pxzDGYkkkPqradV3IGfI36QFCZ6WBL1c8C3/P5gMLS+cswjl+yq -CuwjOLC9LXjSygXJdR4vfKbW9ReOBv9hG9TmDDuR3YmXOYwBcZKxalSrHpC4/n5o -K3a8LX74AHMp7zD9EGuqo0Dr5A0nr5QpgZ4JIWhUNzHKNQEe5lW3Q9tujdZzu9ZT -k6uTRMc+jpyWM6R4/SDo3UE7ClIgXEgswM+vyILFiymSZOxiHQBJFno3bkm1NO8v -+rWrHdy9/1tivQLXh3GWc/uYkytRKgdorbcoZ8D6OOIFaNJlJW6yxZ+V6sAB1K6l -cPOaHch+SGtzPfg5eeW/9QuUuXrDk1M7hvxhV9BBA6Enz7Ns6Zn7gehQSRkUc4aW -HcbkXCxZQzCoUDJPLr7Vw1lrfPgvVfWdxvtgOuDFGmnX4V0xCXI2j4ETtDlyLDi4 -VxoZ9CoyWT99hwEeIr5qa3+4WiMO/3pijKm88gh80thC1udHfNUicv3BG4jrEkyb -k1+2dk9Gath9gs++oymifoRdQP2vrEVb+2Tdxf4AFW/JY+pzOJVL5+sxrX+E5+gT -H5qClep/LdPlgPOYJzldb9aV+t5ku9OkSg30Yoi1+4Y2Rn43zKchL+1YdYJebvAF -btmO4yp5zFUfUjMQ3iRZyQ4VIVQvakojbVfTvMcSW5Vrggk= -=8BHh ------END PGP PUBLIC KEY BLOCK----- diff --git a/scripts/run-podman-build-appimage.sh b/scripts/run-podman-build-appimage.sh index 5dea4b4..5024952 100755 --- a/scripts/run-podman-build-appimage.sh +++ b/scripts/run-podman-build-appimage.sh @@ -3,13 +3,14 @@ set -xe cd "`dirname $(readlink -f ${0})`" -podman build -t chiaki-bionic . -f Dockerfile.bionic +podman build --arch amd64 -t localhost/chiaki-noble . -f Dockerfile.noble cd .. podman run --rm \ + --arch amd64 \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ --device /dev/fuse \ --cap-add SYS_ADMIN \ - -t chiaki-bionic \ + -t localhost/chiaki-noble \ /bin/bash -c "scripts/build-appimage.sh /build/appdir"