commit 4680be3ba902d7d9f6a679e7f7a2eba055acc76b Author: Leo Date: Tue Apr 7 02:03:25 2026 +0200 init: library wrapper done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a235e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,c++ +# Edit at https://www.toptal.com/developers/gitignore?templates=java,c++ + +### C++ ### +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# End of https://www.toptal.com/developers/gitignore/api/java,c++ + + +discord.sh + +*.h \ No newline at end of file diff --git a/DiscordActivity.java b/DiscordActivity.java new file mode 100644 index 0000000..392ba07 --- /dev/null +++ b/DiscordActivity.java @@ -0,0 +1,107 @@ +package com.leohabrom.discord; + +import java.util.Objects; + +public class DiscordActivity { + // Basic Info + public String name = null; + public String state = null; + public String stateUrl = null; + public String details = null; + public String detailsUrl = null; + public int type = -1; + public int status = -1; + public int platform = -1; + + // Assets + public String largeImage = null; + public String largeText = null; + public String largeUrl = null; + public String smallImage = null; + public String smallText = null; + public String smallUrl = null; + public String inviteCoverImage = null; + + // Party + public String partyId = null; + public int partyCurrentSize = -1; + public int partyMaxSize = -1; + public int partyPrivacy = -1; + + // Timestamps (Unix milliseconds) + public long startTimestamp = -1; + public long endTimestamp = -1; + + //Buttons + public String firstButtonLabel = null; + public String firstButtonUrl = null; + public String secondButtonLabel = null; + public String secondButtonUrl = null; + + public String joinSecret = null; + + // Constants + public static final int TYPE_PLAYING = 0; + //public static final int TYPE_STREAMING = 1; + public static final int TYPE_LISTENING = 2; + public static final int TYPE_WATCHING = 3; + public static final int TYPE_COMPETING = 4; + //public static final int TYPE_CUSTOM = 5; + //public static final int TYPE_HANG = 6; + + public static final int STATUS_NAME = 0; + public static final int STATUS_STATE = 1; + public static final int STATUS_DETAIL = 2; + + public static final int PLATFORM_ANDROID = 0; + public static final int PLATFOTM_DESKTOP = 1; + public static final int PLATFORM_EMBEDDED = 2; + public static final int PLATFORM_IOS = 3; + public static final int PLATFORM_PS4 = 4; + public static final int PLATFORM_PS5 = 5; + public static final int PLATFORM_SAMSUNG = 6; + public static final int PLATFORM_XBOX = 7; + + public static final int PARTY_PRIVATE = 0; + public static final int PARTY_PUBLIC = 0; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DiscordActivity that = (DiscordActivity) o; + + return type == that.type && + status == that.status && + // small tolerance for timestamps to account for drift + Math.abs(startTimestamp - that.startTimestamp) < 500 && + Math.abs(endTimestamp - that.endTimestamp) < 500 && + Objects.equals(name, that.name) && + Objects.equals(state, that.state) && + Objects.equals(stateUrl, that.stateUrl) && + Objects.equals(details, that.details) && + Objects.equals(detailsUrl, that.detailsUrl) && + Objects.equals(largeImage, that.largeImage) && + Objects.equals(largeUrl, that.largeUrl) && + Objects.equals(largeText, that.largeText) && + Objects.equals(smallImage, that.smallImage) && + Objects.equals(smallUrl, that.smallUrl) && + Objects.equals(smallText, that.smallText) && + Objects.equals(inviteCoverImage, that.inviteCoverImage) && + Objects.equals(platform, that.platform) && + Objects.equals(partyId, that.partyId) && + Objects.equals(partyCurrentSize, that.partyCurrentSize) && + Objects.equals(partyMaxSize, that.partyMaxSize) && + Objects.equals(partyPrivacy, that.partyPrivacy) && + Objects.equals(firstButtonLabel, that.firstButtonLabel) && + Objects.equals(firstButtonUrl, that.firstButtonUrl) && + Objects.equals(secondButtonLabel, that.secondButtonLabel) && + Objects.equals(secondButtonUrl, that.secondButtonUrl) && + Objects.equals(joinSecret, that.joinSecret); + } + + @Override + public int hashCode() { + return Objects.hash(name, state, stateUrl, details, detailsUrl, type, status, platform, largeImage, largeText, largeUrl, smallImage, smallText, smallUrl, inviteCoverImage, partyId, partyCurrentSize, partyMaxSize, partyPrivacy, startTimestamp, endTimestamp, firstButtonLabel, firstButtonUrl, secondButtonLabel, secondButtonUrl, joinSecret); + } +} \ No newline at end of file diff --git a/DiscordActivityBuilder.java b/DiscordActivityBuilder.java new file mode 100644 index 0000000..b73f580 --- /dev/null +++ b/DiscordActivityBuilder.java @@ -0,0 +1,265 @@ +package com.leohabrom.discord; + +import com.leohabrom.discord.DiscordActivity; + +import java.net.URI; + +public class DiscordActivityBuilder { + public enum Type { + PLAYING(0), + WATCHING(3), + LISTENING(2), + COMPETING(4); + + private final int value; + + Type(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + public enum Status { + NAME(0), + STATE(1), + DETAIL(2); + + private final int value; + + Status(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + public enum Platform { + ANDROID(0), + DESKTOP(1), + EMBEDDED(2), + IOS(3), + PS4(4), + PS5(5), + SAMSUNG(6), + XBOX(7); + + private final int value; + + Platform(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + public enum Privacy { + PRIVATE(0), + PUBLIC(1); + + private final int value; + + Privacy(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + public enum Button { + FIRST(0), + SECOND(1); + + private final int value; + + Button(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + private final DiscordActivity activity; + + public DiscordActivityBuilder() { + activity = new DiscordActivity(); + } + + public DiscordActivityBuilder details(String details) { + activity.details = details; + return this; + } + + public DiscordActivityBuilder details(String details, URI url) { + activity.details = details; + activity.detailsUrl = url.toString(); + return this; + } + + public DiscordActivityBuilder name(String name) { + activity.name = name; + return this; + } + + public DiscordActivityBuilder state(String state) { + activity.state = state; + return this; + } + + public DiscordActivityBuilder state(String state, URI url) { + activity.state = state; + activity.stateUrl = url.toString(); + return this; + } + + public DiscordActivityBuilder type(Type type) { + activity.type = type.getValue(); + return this; + } + + public DiscordActivityBuilder statusDisplay(Status status) { + activity.status = status.getValue(); + return this; + } + + public DiscordActivityBuilder platform(Platform platform) { + activity.platform = platform.getValue(); + return this; + } + + public DiscordActivityBuilder largeImage(String idOrUrl) { + activity.largeImage = idOrUrl; + return this; + } + + public DiscordActivityBuilder largeImage(String idOrUrl, String text) { + activity.largeImage = idOrUrl; + activity.largeText = text; + return this; + } + + public DiscordActivityBuilder largeImage(String idOrUrl, URI url) { + activity.largeImage = idOrUrl; + activity.largeUrl = url.toString(); + return this; + } + + public DiscordActivityBuilder largeImage(String idOrUrl, String text, URI url) { + activity.largeImage = idOrUrl; + activity.largeText = text; + activity.largeUrl = url.toString(); + return this; + } + + public DiscordActivityBuilder smallImage(String idOrUrl) { + activity.smallImage = idOrUrl; + return this; + } + + public DiscordActivityBuilder smallImage(String idOrUrl, String text) { + activity.smallImage = idOrUrl; + activity.smallText = text; + return this; + } + + public DiscordActivityBuilder smallImage(String idOrUrl, URI url) { + activity.smallImage = idOrUrl; + activity.smallUrl = url.toString(); + return this; + } + + public DiscordActivityBuilder smallImage(String idOrUrl, String text, URI url) { + activity.smallImage = idOrUrl; + activity.smallText = text; + activity.smallUrl = url.toString(); + return this; + } + + public DiscordActivityBuilder inviteCoverImage(String idOrUrl) { + activity.inviteCoverImage = idOrUrl; + return this; + } + + public DiscordActivityBuilder party(String id) { + activity.partyId = id; + return this; + } + + + public DiscordActivityBuilder party(String id, int currentSize) { + activity.partyId = id; + activity.partyCurrentSize = currentSize; + return this; + } + + + public DiscordActivityBuilder party(String id, int currentSize, Privacy privacy) { + activity.partyId = id; + activity.partyCurrentSize = currentSize; + activity.partyPrivacy = privacy.getValue(); + return this; + } + + public DiscordActivityBuilder party(String id, Privacy privacy) { + activity.partyId = id; + activity.partyPrivacy = privacy.getValue(); + return this; + } + + public DiscordActivityBuilder party(String id, int currentSize, int maxSize, Privacy privacy) { + activity.partyId = id; + activity.partyCurrentSize = currentSize; + activity.partyMaxSize = maxSize; + activity.partyPrivacy = privacy.getValue(); + return this; + } + + public DiscordActivityBuilder time(long startTime) { + activity.startTimestamp = startTime; + return this; + } + + public DiscordActivityBuilder endTime(long endTime) { + activity.startTimestamp = endTime; + return this; + } + + public DiscordActivityBuilder time(long startTime, long endTime) { + activity.startTimestamp = startTime; + activity.endTimestamp = endTime; + return this; + } + + public DiscordActivityBuilder button(Button id, String label, URI url) { + switch (id) { + case FIRST -> { + activity.firstButtonLabel = label; + activity.firstButtonUrl = url.toString(); + } + case SECOND -> { + activity.secondButtonLabel = label; + activity.secondButtonUrl = url.toString(); + } + } + return this; + } + + public DiscordActivityBuilder joinSecret(String secret) { + activity.joinSecret = secret; + return this; + } + + public DiscordActivity build() { + return activity; + } +} diff --git a/DiscordBridge.cpp b/DiscordBridge.cpp new file mode 100644 index 0000000..f966fa0 --- /dev/null +++ b/DiscordBridge.cpp @@ -0,0 +1,347 @@ +#define DISCORDPP_IMPLEMENTATION +#include "discordpp.h" +#include "DiscordBridge.h" +#include +#include +#include + +std::shared_ptr g_client; +JavaVM *g_jvm = nullptr; +// Helper to check if a Java string field is set and return it +std::string getJString(JNIEnv* env, jobject obj, const char* fieldName) { + jclass objClass = env->GetObjectClass(obj); + jfieldID fid = env->GetFieldID(objClass, fieldName, "Ljava/lang/String;"); + jstring jstr = (jstring)env->GetObjectField(obj, fid); + + if (!jstr) return ""; + + // 1. Call String.getBytes("UTF-8") + jclass stringClass = env->FindClass("java/lang/String"); + jmethodID getBytes = env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B"); + jstring utf8 = env->NewStringUTF("UTF-8"); + jbyteArray array = (jbyteArray)env->CallObjectMethod(jstr, getBytes, utf8); + + // 2. Convert jbyteArray to std::string + jsize len = env->GetArrayLength(array); + jbyte* bytes = env->GetByteArrayElements(array, nullptr); + std::string result((char*)bytes, len); + + // 3. Cleanup + env->ReleaseByteArrayElements(array, bytes, JNI_ABORT); + env->DeleteLocalRef(utf8); + env->DeleteLocalRef(array); + + return result; +} + +void sendToJavaThreadSafe(jobject globalObj, const std::string &message) +{ + if (g_jvm == nullptr) + return; + + JNIEnv *env; + bool attached = false; + jint res = g_jvm->GetEnv((void **)&env, JNI_VERSION_1_6); + + if (res == JNI_EDETACHED) + { + if (g_jvm->AttachCurrentThread((void **)&env, nullptr) != 0) + return; + attached = true; + } + + jclass cls = env->GetObjectClass(globalObj); + jmethodID mid = env->GetMethodID(cls, "onMessage", "(Ljava/lang/String;)V"); + + if (mid != nullptr) + { + jstring jmsg = env->NewStringUTF(message.c_str()); + env->CallVoidMethod(globalObj, mid, jmsg); + env->DeleteLocalRef(jmsg); + } + + if (attached) + g_jvm->DetachCurrentThread(); +} +extern "C" +{ + JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) + { + g_jvm = vm; + return JNI_VERSION_1_6; + } + + JNIEXPORT void JNICALL Java_com_leohabrom_discord_DiscordBridge_init(JNIEnv *env, jobject obj, jlong appId) + { + g_client = std::make_shared(); + g_client->SetApplicationId(static_cast(appId)); + } + + JNIEXPORT void JNICALL Java_com_leohabrom_discord_DiscordBridge_update(JNIEnv *env, jobject obj, jobject jAct) + { + if (!g_client) + { + return; + } + jobject globalObj = env->NewGlobalRef(obj); + + discordpp::Activity activity; + jclass cls = env->GetObjectClass(jAct); + + // 1. Basic Info & URLs + std::string name = getJString(env, jAct, "name"); + if (!name.empty()) + activity.SetName(name); + + std::string state = getJString(env, jAct, "state"); + if (!state.empty()) + { + activity.SetState(state); + std::string stateUrl = getJString(env, jAct, "stateUrl"); + if (!stateUrl.empty()) + activity.SetStateUrl(stateUrl); + } + + std::string details = getJString(env, jAct, "details"); + if (!details.empty()) + { + activity.SetDetails(details); + std::string detailsUrl = getJString(env, jAct, "detailsUrl"); + if (!detailsUrl.empty()) + activity.SetDetailsUrl(detailsUrl); + } + + // 2. Enums (Type, Status Display, Platform) + jint jType = env->GetIntField(jAct, env->GetFieldID(cls, "type", "I")); + switch (jType) + { + case 0: + activity.SetType(discordpp::ActivityTypes::Playing); + break; + // case 1: activity.SetType(discordpp::ActivityTypes::Streaming); break; + case 2: + activity.SetType(discordpp::ActivityTypes::Listening); + break; + case 3: + activity.SetType(discordpp::ActivityTypes::Watching); + break; + case 4: + activity.SetType(discordpp::ActivityTypes::Competing); + break; + // case 5: activity.SetType(discordpp::ActivityTypes::CustomStatus); break; + // case 6: activity.SetType(discordpp::ActivityTypes::HangStatus); break; + // If type is -1 or any other value, default to Playing + default: + break; + } + + jint jStatus = env->GetIntField(jAct, env->GetFieldID(cls, "status", "I")); + switch (jStatus) + { + case 0: + activity.SetStatusDisplayType(discordpp::StatusDisplayTypes::Name); + break; + case 1: + activity.SetStatusDisplayType(discordpp::StatusDisplayTypes::State); + break; + case 2: + activity.SetStatusDisplayType(discordpp::StatusDisplayTypes::Details); + break; + default: + break; // Don't set if -1 + } + + jint jPlatform = env->GetIntField(jAct, env->GetFieldID(cls, "platform", "I")); + switch (jPlatform) + { + case 0: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::Android); + break; + case 1: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::Desktop); + break; + case 2: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::Embedded); + break; + case 3: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::IOS); + break; + case 4: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::PS4); + break; + case 5: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::PS5); + break; + case 6: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::Samsung); + break; + case 7: + activity.SetSupportedPlatforms(discordpp::ActivityGamePlatforms::Xbox); + break; + default: + break; + } + + // 3. Assets + discordpp::ActivityAssets assets; + bool hasAnyAsset = false; + + // Handle Large Image Group + std::string li = getJString(env, jAct, "largeImage"); + if (!li.empty()) + { + assets.SetLargeImage(li); + hasAnyAsset = true; + + // Only set metadata if the image itself exists + std::string lt = getJString(env, jAct, "largeText"); + if (!lt.empty()) + assets.SetLargeText(lt); + + std::string lu = getJString(env, jAct, "largeUrl"); + if (!lu.empty()) + assets.SetLargeUrl(lu); + } + + // Handle Small Image Group (Independent of Large Image) + std::string si = getJString(env, jAct, "smallImage"); + if (!si.empty()) + { + assets.SetSmallImage(si); + hasAnyAsset = true; + + // Only set metadata if the image itself exists + std::string st = getJString(env, jAct, "smallText"); + if (!st.empty()) + assets.SetSmallText(st); + + std::string su = getJString(env, jAct, "smallUrl"); + if (!su.empty()) + assets.SetSmallUrl(su); + } + + std::string ci = getJString(env, jAct, "inviteCoverImage"); + if (!ci.empty()) + { + assets.SetInviteCoverImage(ci); + hasAnyAsset = true; + } + + // Only apply assets to the activity if at least one image was set + if (hasAnyAsset) + { + activity.SetAssets(assets); + } + + // 4. Party & Secrets Logic (Independent & Optional) + discordpp::ActivityParty party; + + // Party ID + std::string pId = getJString(env, jAct, "partyId"); + if (!pId.empty()) + { + party.SetId(pId); + + // Current Size + jint pCur = env->GetIntField(jAct, env->GetFieldID(cls, "partyCurrentSize", "I")); + if (pCur != -1) + { + party.SetCurrentSize(pCur); + } + + // Max Size + jint pMax = env->GetIntField(jAct, env->GetFieldID(cls, "partyMaxSize", "I")); + if (pMax != -1) + { + party.SetMaxSize(pMax); + } + + // Privacy + jint pPriv = env->GetIntField(jAct, env->GetFieldID(cls, "partyPrivacy", "I")); + if (pPriv != -1) + { + // 0 = Private, 1 = Public (based on your Java constants) + if (pPriv == 0) + party.SetPrivacy(discordpp::ActivityPartyPrivacy::Private); + else if (pPriv == 1) + party.SetPrivacy(discordpp::ActivityPartyPrivacy::Public); + } + + activity.SetParty(party); + } + + // 5. Timestamps Logic (Independent & Optional) + discordpp::ActivityTimestamps ts; + bool hasTimestamps = false; + + jlong start = env->GetLongField(jAct, env->GetFieldID(cls, "startTimestamp", "J")); + if (start != -1) + { + ts.SetStart(static_cast(start)); + hasTimestamps = true; + } + + jlong end = env->GetLongField(jAct, env->GetFieldID(cls, "endTimestamp", "J")); + if (end != -1) + { + ts.SetEnd(static_cast(end)); + hasTimestamps = true; + } + + if (hasTimestamps) + { + activity.SetTimestamps(ts); + } + + // 6. Buttons + bool hasButton = false; + + std::string b1Label = getJString(env, jAct, "firstButtonLabel"); + std::string b1Url = getJString(env, jAct, "firstButtonUrl"); + + if (!b1Label.empty() && !b1Url.empty()) + { + discordpp::ActivityButton b1; + b1.SetLabel(b1Label); + b1.SetUrl(b1Url); + hasButton = true; + activity.AddButton(b1); + } + + std::string b2Label = getJString(env, jAct, "secondButtonLabel"); + std::string b2Url = getJString(env, jAct, "secondButtonUrl"); + + if (!b2Label.empty() && !b2Url.empty()) + { + discordpp::ActivityButton b2; + b2.SetLabel(b2Label); + b2.SetUrl(b2Url); + hasButton = true; + activity.AddButton(b2); + } + + // Join Secret (Independent) + std::string joinSec = getJString(env, jAct, "joinSecret"); + if (!joinSec.empty() && !hasButton) + { + discordpp::ActivitySecrets secrets; + secrets.SetJoin(joinSec); + activity.SetSecrets(secrets); + } + + g_client->UpdateRichPresence(activity, [globalObj](const discordpp::ClientResult &r) + { + std::cout << r.ToString() << std::endl; + if (r.Successful()) sendToJavaThreadSafe(globalObj, "Update Sent"); + else sendToJavaThreadSafe(globalObj, "Update Failed"); + JNIEnv* t_env; + if (g_jvm->GetEnv((void**)&t_env, JNI_VERSION_1_6) == JNI_OK) { + t_env->DeleteGlobalRef(globalObj); + } }); + } + + JNIEXPORT void JNICALL Java_com_leohabrom_discord_DiscordBridge_runCallbacks(JNIEnv *env, jobject obj) + { + discordpp::RunCallbacks(); + } +} \ No newline at end of file diff --git a/DiscordBridge.java b/DiscordBridge.java new file mode 100644 index 0000000..fa4903a --- /dev/null +++ b/DiscordBridge.java @@ -0,0 +1,36 @@ +package com.leohabrom.discord; +import com.leohabrom.discord.DiscordBridgeAdapter; + +public class DiscordBridge { + static { + System.loadLibrary("discord_bridge"); + } + + public native void init(long appId); + public native void update(DiscordActivity activity); + public native void runCallbacks(); + private DiscordBridgeAdapter adapter = null; + + public void start() { + Thread heartbeat = new Thread(() -> { + try { + while (!Thread.currentThread().isInterrupted()) { + runCallbacks(); + Thread.sleep(16); + } + } catch (InterruptedException e) {} + }); + heartbeat.setDaemon(true); + heartbeat.start(); + } + + + + public void setAdapter(DiscordBridgeAdapter adapter) { + this.adapter = adapter; + } + + public void onMessage(String string) { + if (adapter!=null) adapter.onMessage(string); + } +} \ No newline at end of file diff --git a/DiscordBridgeAdapter.java b/DiscordBridgeAdapter.java new file mode 100644 index 0000000..315ef30 --- /dev/null +++ b/DiscordBridgeAdapter.java @@ -0,0 +1,5 @@ +package com.leohabrom.discord; + +public interface DiscordBridgeAdapter { + void onMessage(String message); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..002a7f3 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +``` +export DISCORD_SDK= +./export.sh +``` \ No newline at end of file diff --git a/export.sh b/export.sh new file mode 100755 index 0000000..5227566 --- /dev/null +++ b/export.sh @@ -0,0 +1,20 @@ +rm *.class + +javac -g -d . DiscordActivity.java DiscordActivityBuilder.java DiscordBridgeAdapter.java DiscordBridge.java + +jar cvf out/lib/DiscordLib.jar com/ DiscordActivity.java DiscordActivityBuilder.java DiscordBridgeAdapter.java DiscordBridge.java +javac -h . DiscordActivity.java DiscordActivityBuilder.java DiscordBridgeAdapter.java DiscordBridge.java + + +g++ -shared -fPIC \ +-I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" \ +-I"$DISCORD_SDK/include" \ +DiscordBridge.cpp \ +-L"$DISCORD_SDK/lib/release" \ +-ldiscord_partner_sdk \ +-Wl,-rpath,'$ORIGIN' \ +-o out/lib/libdiscord_bridge.so + +cp "$DISCORD_SDK/lib/release/libdiscord_partner_sdk.so" out/lib/ + +zip out/libraries.zip out/lib/DiscordLib.jar out/lib/libdiscord_bridge.so out/lib/libdiscord_partner_sdk.so \ No newline at end of file