├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── docs.yml ├── .gitignore ├── .vscode ├── c_cpp_properties.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── bridge.c ├── bridge.ico ├── bridge.manifest ├── bridge.rc ├── build ├── bridge.sh └── launchd.sh ├── docs ├── README.md ├── assets │ ├── contentwarning.png │ ├── favicon.png │ ├── flatseal_permission.png │ ├── gui.png │ ├── hades.png │ ├── lethalcompany.png │ ├── lutris.png │ ├── lutris_lol.png │ ├── macos-crossover.webm │ ├── steam_amongus.png │ ├── steam_protontricks.png │ ├── steam_script.png │ └── vividstasis.png ├── index.md ├── linux.md ├── macos.md └── usage.md ├── game.c ├── gui.c ├── main.c ├── mkdocs.yml ├── resource.h └── service.c /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: EnderIce2 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Log File** 24 | Located in `/path/to/prefix/drive_c/windows/logs/bridge.log` or a screenshot with the terminal should suffice. 25 | 26 | **System Info (please complete the following information):** 27 | - OS: [e.g. SteamOS 3.0, Ubuntu 22.04, macOS 15, or output from command `uname -srm`] 28 | - Wine: [e.g. 10.5] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: EnderIce2 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Project 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | paths-ignore: 7 | - 'ISSUE_TEMPLATE/**' 8 | - 'workflows/**' 9 | - '.vsocde/**' 10 | - 'docs/**' 11 | pull_request: 12 | branches: [ "master" ] 13 | paths-ignore: 14 | - 'ISSUE_TEMPLATE/**' 15 | - 'workflows/**' 16 | - '.vsocde/**' 17 | - 'docs/**' 18 | 19 | permissions: 20 | contents: write 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: dependencies 28 | run: sudo apt update && sudo apt -y install gcc-mingw-w64 make 29 | - name: make 30 | run: make 31 | - name: artifact 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: bridge 35 | path: build 36 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths: 7 | - docs/** 8 | - mkdocs.yml 9 | pull_request: 10 | branches: [ "master" ] 11 | paths: 12 | - docs/** 13 | - mkdocs.yml 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Configure Git Credentials 24 | run: | 25 | git config user.name github-actions[bot] 26 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: 3.x 30 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 31 | - uses: actions/cache@v4 32 | with: 33 | key: mkdocs-material-${{ env.cache_id }} 34 | path: .cache 35 | restore-keys: | 36 | mkdocs-material- 37 | - run: pip install mkdocs-material 38 | - run: pip install mkdocs-video 39 | - run: mkdocs gh-deploy --force 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.exe 3 | *.res 4 | *.elf 5 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Windows", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [ 9 | "GIT_COMMIT", 10 | "GIT_BRANCH" 11 | ], 12 | "cStandard": "c17", 13 | "cppStandard": "gnu++17", 14 | "intelliSenseMode": "windows-gcc-x64" 15 | } 16 | ], 17 | "version": 4 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.default.compilerPath": "/usr/bin/x86_64-w64-mingw32-gcc", 3 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | enderice2@protonmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 EnderIce2 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | C_SOURCES = $(shell find ./ -type f -name '*.c') 2 | C_OBJECTS = $(C_SOURCES:.c=.o) 3 | 4 | GIT_COMMIT = $(shell git rev-parse --short HEAD) 5 | GIT_BRANCH = $(shell git rev-parse --abbrev-ref HEAD) 6 | 7 | CWARNFLAGS = -Wno-int-conversion -Wno-incompatible-pointer-types 8 | 9 | CFLAGS = -std=c17 -DGIT_COMMIT='"$(GIT_COMMIT)"' -DGIT_BRANCH='"$(GIT_BRANCH)"' 10 | LFLAGS = -lgdi32 -lws2_32 11 | 12 | # DBGFLAGS = -Wl,--export-all-symbols -g -O0 -ggdb3 -Wall 13 | 14 | all: build 15 | 16 | build: $(C_OBJECTS) 17 | $(info Linking) 18 | x86_64-w64-mingw32-windres bridge.rc -O coff -o bridge.res 19 | x86_64-w64-mingw32-gcc $(C_OBJECTS) bridge.res $(LFLAGS) $(DBGFLAGS) -o build/bridge.exe 20 | 21 | %.o: %.c 22 | $(info Compiling $<) 23 | x86_64-w64-mingw32-gcc $(CFLAGS) $(CWARNFLAGS) $(DBGFLAGS) -c $< -o $@ 24 | 25 | clean: 26 | rm -f $(C_OBJECTS) build/bridge.exe bridge.res 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord RPC Bridge for Wine 2 | 3 | ![GitHub License](https://img.shields.io/github/license/EnderIce2/rpc-bridge?style=for-the-badge) 4 | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/EnderIce2/rpc-bridge/total?style=for-the-badge) 5 | ![GitHub Release](https://img.shields.io/github/v/release/EnderIce2/rpc-bridge?style=for-the-badge) 6 | 7 | Simple bridge that allows you to use Discord Rich Presence with Wine games/software. 8 | 9 | Works by running a small program in the background that creates a [named pipe](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) `\\.\pipe\discord-ipc-0` inside the prefix and forwards all data to the pipe `/run/user/1000/discord-ipc-0`. 10 | 11 | This bridge takes advantage of the Windows service implementation in Wine, eliminating the need to manually run any programs. 12 | 13 | --- 14 | 15 | ## Installation & Usage 16 | 17 | Installation will copy itself to `C:\windows\bridge.exe` and create a Windows service. 18 | Logs are stored in `C:\windows\logs\bridge.log`. 19 | 20 | #### Installing inside a prefix 21 | 22 | ##### Wine (~/.wine) 23 | 24 | - Double click `bridge.exe` and click `Install`. 25 | - ![gui](docs/assets/gui.png) 26 | - To remove, the same process can be followed, but click `Remove` instead. 27 | 28 | *Note, an [extra step](https://github.com/EnderIce2/rpc-bridge?tab=readme-ov-file#macos) is needed on MacOS* 29 | 30 | ##### Lutris 31 | 32 | - Click on a game and select `Run EXE inside Wine prefix`. 33 | - ![lutris](docs/assets/lutris.png) 34 | - The same process can be followed as in Wine. 35 | 36 | ##### Steam 37 | 38 | - Right click on the game and select `Properties`. 39 | - Under `Set Launch Options`, add the following: 40 | - ![bridge.sh](docs/assets/steam_script.png "Set Launch Options to the path of the bridge.sh") 41 | - The `bridge.sh` script must be in the same directory as `bridge.exe`. 42 | 43 | #### If you use Flatpak 44 | 45 | - If you are running Steam, Lutris, etc in a Flatpak, you will need to allow the bridge to access the `/run/user/1000/discord-ipc-0` file. 46 | - ##### By using [Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) 47 | - Add `xdg-run/discord-ipc-0` under `Filesystems` category 48 | - ![flatseal](docs/assets/flatseal_permission.png) 49 | - ##### By using the terminal 50 | - Per application 51 | - `flatpak override --filesystem=xdg-run/discord-ipc-0 ` 52 | - Globally 53 | - `flatpak override --user --filesystem=xdg-run/discord-ipc-0` 54 | 55 | ##### MacOS 56 | 57 | The steps for MacOS are almost the same, but due to the way `$TMPDIR` works, you will have to install a **LaunchAgent**. 58 | 59 | - Download the latest build from the [releases](https://github.com/EnderIce2/rpc-bridge/releases) 60 | - Open the archive and make the `launchd.sh` script executable by doing: `chmod +x launchd.sh` 61 | - To **install** the LaunchAgent, run `./launchd.sh install` and to **remove** it simply run `./launchd.sh remove`. 62 | 63 | The script will add a LaunchAgent to your user, that will symlink the `$TMPDIR` directory to `/tmp/rpc-bridge/tmpdir`. 64 | 65 | *Note: You will need to launch the `bridge.exe` file manually in Wine at least once for it to register and launch automatically the next time.* 66 | 67 | More details on how to install the LaunchAgent can be found in the [documentation](https://enderice2.github.io/rpc-bridge/). 68 | 69 | ## Compiling from source 70 | 71 | - Install the `wine`, `gcc-mingw-w64` and `make` packages. 72 | - Open a terminal in the directory that contains this file and run `make`. 73 | - The compiled executable will be located in `build/bridge.exe`. 74 | 75 | ## Credits 76 | 77 | This project is inspired by [wine-discord-ipc-bridge](https://github.com/0e4ef622/wine-discord-ipc-bridge). 78 | 79 | --- 80 | -------------------------------------------------------------------------------- /bridge.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define __linux_read 3 8 | #define __linux_write 4 9 | #define __linux_open 5 10 | #define __linux_close 6 11 | #define __linux_munmap 91 12 | #define __linux_socketcall 102 13 | #define __linux_mmap2 192 14 | #define __linux_socket 359 15 | #define __linux_connect 362 16 | 17 | #define __darwin_read 0x2000003 18 | #define __darwin_write 0x2000004 19 | #define __darwin_open 0x2000005 20 | #define __darwin_close 0x2000006 21 | #define __darwin_socket 0x2000061 22 | #define __darwin_connect 0x2000062 23 | #define __darwin_mmap 0x20000C5 24 | #define __darwin_fcntl 0x200005C 25 | #define __darwin_sysctl 0x20000CA 26 | 27 | #define O_RDONLY 00 28 | 29 | /* macos & linux are the same for PROT_READ, PROT_WRITE, MAP_FIXED & MAP_PRIVATE */ 30 | #define PROT_READ 1 31 | #define PROT_WRITE 2 32 | #define MAP_PRIVATE 0x02 33 | #define MAP_FIXED 0x10 34 | #define MAP_ANON 0x20 35 | #define MAP_FAILED ((void *)-1) 36 | 37 | #define __darwin_MAP_ANON 0x1000 38 | 39 | #define SYS_SOCKET 1 40 | #define SYS_CONNECT 3 41 | 42 | #define likely(expr) (__builtin_expect(!!(expr), 1)) 43 | #define unlikely(expr) (__builtin_expect(!!(expr), 0)) 44 | 45 | #define force_inline \ 46 | __inline__ \ 47 | __attribute__((__always_inline__, __gnu_inline__)) 48 | #define naked __attribute__((naked)) 49 | 50 | #define BUFFER_LENGTH 2048 51 | 52 | typedef unsigned short sa_family_t; 53 | typedef char *caddr_t; 54 | typedef unsigned socklen_t; 55 | struct sockaddr_un 56 | { 57 | sa_family_t sun_family; /* AF_UNIX */ 58 | char sun_path[108]; /* Pathname */ 59 | }; 60 | 61 | typedef struct 62 | { 63 | int fd; 64 | HANDLE hPipe; 65 | } bridge_thread; 66 | 67 | void print(char const *fmt, ...); 68 | LPTSTR GetErrorMessage(); 69 | extern BOOL RunningAsService; 70 | BOOL RetryNewConnection; 71 | BOOL IsLinux; 72 | HANDLE hOut = NULL; 73 | HANDLE hIn = NULL; 74 | 75 | static force_inline int linux_syscall(int num, 76 | int arg1, int arg2, int arg3, 77 | int arg4, int arg5, int arg6) 78 | { 79 | int ret; 80 | __asm__ __volatile__( 81 | "int $0x80\n" 82 | : "=a"(ret) 83 | : "0"(num), "b"(arg1), "c"(arg2), 84 | "d"(arg3), "S"(arg4), "D"(arg5) 85 | : "memory"); 86 | return ret; 87 | } 88 | 89 | static naked int darwin_syscall(int num, 90 | long arg1, long arg2, long arg3, 91 | long arg4, long arg5, long arg6) 92 | { 93 | register long r10 __asm__("r10") = arg4; 94 | register long r8 __asm__("r8") = arg5; 95 | register long r9 __asm__("r9") = arg6; 96 | __asm__ __volatile__( 97 | "syscall\n" 98 | "jae noerror\n" 99 | "negq %%rax\n" 100 | "noerror:\n" 101 | "ret\n" 102 | : "=a"(num) 103 | : "a"(num), "D"(arg1), "S"(arg2), "d"(arg3), "r"(r10), "r"(r8), "r"(r9) 104 | : "memory"); 105 | } 106 | 107 | static inline int sys_read(int fd, void *buf, size_t count) 108 | { 109 | if (IsLinux) 110 | return linux_syscall(__linux_read, fd, buf, count, 0, 0, 0); 111 | else 112 | return darwin_syscall(__darwin_read, fd, buf, count, 0, 0, 0); 113 | } 114 | 115 | static inline int sys_write(int fd, const void *buf, size_t count) 116 | { 117 | if (IsLinux) 118 | return linux_syscall(__linux_write, fd, buf, count, 0, 0, 0); 119 | else 120 | return darwin_syscall(__darwin_write, fd, buf, count, 0, 0, 0); 121 | } 122 | 123 | static inline int sys_open(const char *pathname, int flags, int mode) 124 | { 125 | if (IsLinux) 126 | return linux_syscall(__linux_open, pathname, flags, mode, 0, 0, 0); 127 | else 128 | return darwin_syscall(__darwin_open, pathname, flags, mode, 0, 0, 0); 129 | } 130 | 131 | static inline int sys_close(int fd) 132 | { 133 | if (IsLinux) 134 | return linux_syscall(__linux_close, fd, 0, 0, 0, 0, 0); 135 | else 136 | return darwin_syscall(__darwin_close, fd, 0, 0, 0, 0, 0); 137 | } 138 | 139 | static inline unsigned int *sys_mmap(unsigned int *addr, size_t length, int prot, int flags, int fd, off_t offset) 140 | { 141 | if (IsLinux) 142 | return linux_syscall(__linux_mmap2, addr, length, prot, flags, fd, offset); 143 | else 144 | { 145 | if (flags & MAP_ANON) 146 | { 147 | flags &= ~MAP_ANON; 148 | flags |= __darwin_MAP_ANON; 149 | } 150 | return darwin_syscall(__darwin_mmap, addr, length, prot, flags, fd, offset); 151 | } 152 | } 153 | 154 | static inline int sys_munmap(unsigned int *addr, size_t length) 155 | { 156 | assert(IsLinux); 157 | return linux_syscall(__linux_munmap, addr, length, 0, 0, 0, 0); 158 | } 159 | 160 | static inline int sys_socketcall(int call, unsigned long *args) 161 | { 162 | assert(IsLinux); 163 | return linux_syscall(__linux_socketcall, call, args, 0, 0, 0, 0); 164 | } 165 | 166 | static inline int sys_socket(int domain, int type, int protocol) 167 | { 168 | if (IsLinux) 169 | return linux_syscall(__linux_socket, domain, type, protocol, 0, 0, 0); 170 | else 171 | return darwin_syscall(__darwin_socket, domain, type, protocol, 0, 0, 0); 172 | } 173 | 174 | static inline int sys_connect(int s, caddr_t name, socklen_t namelen) 175 | { 176 | if (IsLinux) 177 | return linux_syscall(__linux_connect, s, name, namelen, 0, 0, 0); 178 | else 179 | return darwin_syscall(__darwin_connect, s, name, namelen, 0, 0, 0); 180 | } 181 | 182 | void *environStr = NULL; 183 | char *native_getenv(const char *name) 184 | { 185 | static char lpBuffer[512]; 186 | DWORD ret = GetEnvironmentVariable("BRIDGE_RPC_PATH", lpBuffer, sizeof(lpBuffer)); 187 | if (ret != 0) 188 | return lpBuffer; 189 | 190 | if (!IsLinux) 191 | { 192 | char *value = getenv(name); 193 | if (value == NULL) 194 | { 195 | print("Failed to get environment variable: %s\n", name); 196 | 197 | /* Use GetEnvironmentVariable as a last resort */ 198 | DWORD ret = GetEnvironmentVariable(name, lpBuffer, sizeof(lpBuffer)); 199 | if (ret == 0) 200 | { 201 | print("GetEnvironmentVariable(\"%s\", ...) failed: %d\n", name, ret); 202 | return NULL; 203 | } 204 | return lpBuffer; 205 | } 206 | return value; 207 | } 208 | 209 | /* I hope the 0x20000 is okay */ 210 | if (environStr == NULL) 211 | environStr = sys_mmap(0x20000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); 212 | 213 | if ((uintptr_t)environStr > 0x7effffff) 214 | print("Warning: environStr %#lx is above 2GB\n", environStr); 215 | 216 | const char *linux_environ = "/proc/self/environ"; 217 | memcpy(environStr, linux_environ, strlen(linux_environ) + 1); 218 | 219 | int fd = sys_open(environStr, O_RDONLY, 0); 220 | 221 | if (fd < 0) 222 | { 223 | print("Failed to open /proc/self/environ: %d\n", fd); 224 | return NULL; 225 | } 226 | 227 | char *buffer = sys_mmap(0x22000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); 228 | char *result = NULL; 229 | int bytesRead; 230 | 231 | while ((bytesRead = sys_read(fd, buffer, 0x1000 - 1)) > 0) 232 | { 233 | buffer[bytesRead] = '\0'; 234 | char *env = buffer; 235 | while (*env) 236 | { 237 | if (strstr(env, name) == env) 238 | { 239 | env += strlen(name); 240 | if (*env == '=') 241 | { 242 | env++; 243 | result = strdup(env); 244 | break; 245 | } 246 | } 247 | env += strlen(env) + 1; 248 | } 249 | 250 | if (result) 251 | break; 252 | } 253 | 254 | sys_close(fd); 255 | return result; 256 | } 257 | 258 | void ConnectToSocket(int fd) 259 | { 260 | print("Connecting to socket\n"); 261 | const char *runtime; 262 | if (IsLinux) 263 | runtime = native_getenv("XDG_RUNTIME_DIR"); 264 | else 265 | { 266 | runtime = native_getenv("TMPDIR"); 267 | if (runtime == NULL) 268 | { 269 | runtime = "/tmp/rpc-bridge/tmpdir"; 270 | print("IPC directory not set, fallback to /tmp/rpc-bridge/tmpdir\n"); 271 | 272 | // Check if the directory exists 273 | DWORD dwAttrib = GetFileAttributes(runtime); 274 | if (dwAttrib == INVALID_FILE_ATTRIBUTES || !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY)) 275 | { 276 | print("IPC directory does not exist: %s. If you're on MacOS, see the github guide on how to install the launchd service.\n", runtime); 277 | // Handle the case where the directory doesn't exist 278 | // For example, create the directory 279 | 280 | int result = MessageBox(NULL, "IPC directory does not exist\nDo you want to open the installation guide?", 281 | "Directory not found", 282 | MB_YESNO | MB_ICONSTOP); 283 | if (result == IDYES) 284 | ShellExecute(NULL, "open", "https://enderice2.github.io/rpc-bridge/installation.html#macos", NULL, NULL, SW_SHOWNORMAL); 285 | ExitProcess(1); 286 | } 287 | } 288 | } 289 | 290 | print("IPC directory: %s\n", runtime); 291 | 292 | /* TODO: check for multiple discord instances and create a pipe for each */ 293 | const char *discordUnixSockets[] = { 294 | "%s/discord-ipc-%d", 295 | "%s/app/com.discordapp.Discord/discord-ipc-%d", 296 | "%s/.flatpak/dev.vencord.Vesktop/xdg-run/discord-ipc-%d", 297 | "%s/snap.discord/discord-ipc-%d", 298 | "%s/snap.discord-canary/discord-ipc-%d", 299 | }; 300 | 301 | int sockRet = 0; 302 | for (int i = 0; i < sizeof(discordUnixSockets) / sizeof(discordUnixSockets[0]); i++) 303 | { 304 | size_t pipePathLen = strlen(runtime) + strlen(discordUnixSockets[i]) + 1; 305 | char *pipePath = malloc(pipePathLen); 306 | 307 | for (int j = 0; j < 16; j++) 308 | { 309 | if (IsLinux) 310 | { 311 | struct sockaddr_un *socketAddr = sys_mmap(0x23000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); 312 | print("Socket address allocated at %#lx\n", socketAddr); 313 | socketAddr->sun_family = AF_UNIX; 314 | 315 | snprintf(pipePath, pipePathLen, discordUnixSockets[i], runtime, j); 316 | strcpy_s(socketAddr->sun_path, sizeof(socketAddr->sun_path), pipePath); 317 | print("Probing %s\n", pipePath); 318 | 319 | // unsigned long socketArgs[] = { 320 | // (unsigned long)fd, 321 | // (unsigned long)(intptr_t)&socketAddr, 322 | // sizeof(socketAddr)}; 323 | unsigned long *socketArgs = sys_mmap(0x24000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); 324 | socketArgs[0] = (unsigned long)fd; 325 | socketArgs[1] = (unsigned long)(intptr_t)socketAddr; 326 | socketArgs[2] = sizeof(struct sockaddr_un); 327 | socketArgs[3] = 0; 328 | 329 | sockRet = sys_socketcall(SYS_CONNECT, socketArgs); 330 | } 331 | else 332 | { 333 | struct sockaddr_un socketAddr; 334 | socketAddr.sun_family = AF_UNIX; 335 | 336 | snprintf(pipePath, pipePathLen, discordUnixSockets[i], runtime, j); 337 | strcpy_s(socketAddr.sun_path, sizeof(socketAddr.sun_path), pipePath); 338 | print("Probing %s\n", pipePath); 339 | 340 | sockRet = sys_connect(fd, (caddr_t)&socketAddr, sizeof(socketAddr)); 341 | } 342 | 343 | print(" error: %d\n", sockRet); 344 | if (sockRet >= 0) 345 | break; 346 | } 347 | 348 | if (sockRet >= 0) 349 | { 350 | print("Connecting to %s\n", pipePath); 351 | free(pipePath); 352 | break; 353 | } 354 | free(pipePath); 355 | } 356 | 357 | if (sockRet < 0) 358 | { 359 | print("socketcall failed for: %d\n", sockRet); 360 | if (!RunningAsService) 361 | MessageBox(NULL, "Failed to connect to Discord", 362 | "Socket Connection failed", 363 | MB_OK | MB_ICONSTOP); 364 | ExitProcess(1); 365 | } 366 | } 367 | 368 | void PipeBufferInThread(LPVOID lpParam) 369 | { 370 | bridge_thread *bt = (bridge_thread *)lpParam; 371 | print("In thread started using fd %d and pipe %#x\n", bt->fd, bt->hPipe); 372 | int EOFCount = 0; 373 | char *l_buffer; 374 | if (IsLinux) 375 | l_buffer = sys_mmap(0x25000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); 376 | else 377 | l_buffer = malloc(BUFFER_LENGTH); 378 | print("Buffer in thread allocated at %#lx\n", l_buffer); 379 | while (TRUE) 380 | { 381 | char buffer[BUFFER_LENGTH]; 382 | int read = sys_read(bt->fd, l_buffer, BUFFER_LENGTH); 383 | 384 | if (unlikely(read < 0)) 385 | { 386 | print("Failed to read from unix pipe: %d\n", read); 387 | Sleep(1000); 388 | continue; 389 | } 390 | 391 | if (EOFCount > 4) 392 | { 393 | print("EOF count exceeded\n"); 394 | RetryNewConnection = TRUE; 395 | TerminateThread(hOut, 0); 396 | break; 397 | } 398 | 399 | if (unlikely(read == 0)) 400 | { 401 | print("EOF\n"); 402 | Sleep(1000); 403 | EOFCount++; 404 | continue; 405 | } 406 | EOFCount = 0; 407 | 408 | memcpy(buffer, l_buffer, read); 409 | 410 | print("Reading %d bytes from unix pipe: \"", read); 411 | for (int i = 0; i < read; i++) 412 | print("%c", buffer[i]); 413 | print("\"\n"); 414 | 415 | DWORD dwWritten; 416 | WINBOOL bResult = WriteFile(bt->hPipe, buffer, read, &dwWritten, NULL); 417 | if (unlikely(bResult == FALSE)) 418 | { 419 | if (GetLastError() == ERROR_BROKEN_PIPE) 420 | { 421 | RetryNewConnection = TRUE; 422 | print("In Broken pipe\n"); 423 | break; 424 | } 425 | 426 | print("Failed to read from pipe: %s\n", GetErrorMessage()); 427 | Sleep(1000); 428 | continue; 429 | } 430 | 431 | if (unlikely(dwWritten < 0)) 432 | { 433 | print("Failed to write to pipe: %s\n", GetErrorMessage()); 434 | Sleep(1000); 435 | continue; 436 | } 437 | 438 | while (dwWritten < read) 439 | { 440 | int last_written = dwWritten; 441 | WINBOOL bResult = WriteFile(bt->hPipe, buffer + dwWritten, read - dwWritten, &dwWritten, NULL); 442 | if (unlikely(bResult == FALSE)) 443 | { 444 | if (GetLastError() == ERROR_BROKEN_PIPE) 445 | { 446 | RetryNewConnection = TRUE; 447 | print("In Broken pipe\n"); 448 | break; 449 | } 450 | 451 | print("Failed to read from pipe: %s\n", GetErrorMessage()); 452 | Sleep(1000); 453 | continue; 454 | } 455 | 456 | if (unlikely(last_written == dwWritten)) 457 | { 458 | print("Failed to write to pipe: %s\n", GetErrorMessage()); 459 | Sleep(1000); 460 | continue; 461 | } 462 | } 463 | } 464 | } 465 | 466 | void PipeBufferOutThread(LPVOID lpParam) 467 | { 468 | bridge_thread *bt = (bridge_thread *)lpParam; 469 | print("Out thread started using fd %d and pipe %#x\n", bt->fd, bt->hPipe); 470 | char *l_buffer; 471 | if (IsLinux) 472 | l_buffer = sys_mmap(0x26000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); 473 | else 474 | l_buffer = malloc(BUFFER_LENGTH); 475 | print("Buffer out thread allocated at %#lx\n", l_buffer); 476 | while (TRUE) 477 | { 478 | char buffer[BUFFER_LENGTH]; 479 | DWORD dwRead; 480 | WINBOOL bResult = ReadFile(bt->hPipe, buffer, BUFFER_LENGTH, &dwRead, NULL); 481 | if (unlikely(bResult == FALSE)) 482 | { 483 | if (GetLastError() == ERROR_BROKEN_PIPE) 484 | { 485 | RetryNewConnection = TRUE; 486 | print("Out Broken pipe\n"); 487 | break; 488 | } 489 | 490 | print("Failed to read from pipe: %s\n", GetErrorMessage()); 491 | Sleep(1000); 492 | continue; 493 | } 494 | 495 | print("Writing %d bytes to unix pipe: \"", dwRead); 496 | for (int i = 0; i < dwRead; i++) 497 | print("%c", buffer[i]); 498 | print("\"\n"); 499 | 500 | memcpy(l_buffer, buffer, dwRead); 501 | int written = sys_write(bt->fd, l_buffer, dwRead); 502 | if (unlikely(written < 0)) 503 | { 504 | print("Failed to write to socket: %d\n", written); 505 | continue; 506 | } 507 | 508 | while (written < dwRead) 509 | { 510 | int last_written = written; 511 | written += sys_write(bt->fd, buffer + written, dwRead - written); 512 | if (unlikely(last_written == written)) 513 | { 514 | print("Failed to write to socket: %s\n", GetErrorMessage()); 515 | Sleep(1000); 516 | continue; 517 | } 518 | } 519 | } 520 | } 521 | 522 | void CreateBridge() 523 | { 524 | LPCTSTR lpszPipename = TEXT("\\\\.\\pipe\\discord-ipc-0"); 525 | 526 | NewConnection: 527 | if (GetNamedPipeInfo((HANDLE)lpszPipename, 528 | NULL, NULL, 529 | NULL, NULL)) 530 | { 531 | print("Pipe already exists: %s\n", 532 | GetErrorMessage()); 533 | if (!RunningAsService) 534 | { 535 | MessageBox(NULL, GetErrorMessage(), 536 | "Pipe already exists", 537 | MB_OK | MB_ICONSTOP); 538 | } 539 | ExitProcess(1); 540 | } 541 | 542 | HANDLE hPipe = 543 | CreateNamedPipe("\\\\.\\pipe\\discord-ipc-0", 544 | PIPE_ACCESS_DUPLEX, 545 | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 546 | PIPE_UNLIMITED_INSTANCES, BUFFER_LENGTH, BUFFER_LENGTH, 0, NULL); 547 | 548 | if (hPipe == INVALID_HANDLE_VALUE) 549 | { 550 | print("Failed to create pipe: %s\n", 551 | GetErrorMessage()); 552 | if (!RunningAsService) 553 | { 554 | MessageBox(NULL, GetErrorMessage(), 555 | "Failed to create pipe", 556 | MB_OK | MB_ICONSTOP); 557 | } 558 | ExitProcess(1); 559 | } 560 | 561 | print("Pipe %s(%#x) created\n", lpszPipename, hPipe); 562 | print("Waiting for pipe connection\n"); 563 | if (!ConnectNamedPipe(hPipe, NULL)) 564 | { 565 | print("Failed to connect to pipe: %s\n", 566 | GetErrorMessage()); 567 | if (!RunningAsService) 568 | MessageBox(NULL, GetErrorMessage(), 569 | NULL, MB_OK | MB_ICONSTOP); 570 | ExitProcess(1); 571 | } 572 | print("Pipe connected\n"); 573 | 574 | int fd; 575 | if (IsLinux) 576 | { 577 | // unsigned long socketArgs[] = { 578 | // (unsigned long)AF_UNIX, 579 | // (unsigned long)SOCK_STREAM, 580 | // 0}; 581 | unsigned long *socketArgs = sys_mmap(0x21000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); 582 | socketArgs[0] = (unsigned long)AF_UNIX; 583 | socketArgs[1] = (unsigned long)SOCK_STREAM; 584 | socketArgs[2] = 0; 585 | fd = sys_socketcall(SYS_SOCKET, socketArgs); 586 | 587 | /* FIXME: WSAEAFNOSUPPORT: https://gitlab.winehq.org/wine/wine/-/merge_requests/2786 */ 588 | // WSADATA wsaData; 589 | // int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); 590 | // if (iResult != 0) 591 | // printf("WSAStartup failed: %d\n", iResult); 592 | // fd = socket(AF_UNIX, SOCK_STREAM, 0); 593 | } 594 | else 595 | fd = sys_socket(AF_UNIX, SOCK_STREAM, 0); 596 | 597 | if (fd == INVALID_SOCKET) 598 | { 599 | print("invalid socket: %d %d\n", fd, WSAGetLastError()); 600 | ExitProcess(1); 601 | } 602 | 603 | if (fd < 0) 604 | { 605 | print("Failed to create socket: %d\n", fd); 606 | if (!RunningAsService) 607 | MessageBox(NULL, "Failed to create socket", 608 | NULL, MB_OK | MB_ICONSTOP); 609 | ExitProcess(1); 610 | } 611 | 612 | print("Socket %d created\n", fd); 613 | 614 | ConnectToSocket(fd); 615 | print("Connected to Discord\n"); 616 | 617 | bridge_thread bt = {fd, hPipe}; 618 | 619 | hIn = CreateThread(NULL, 0, 620 | (LPTHREAD_START_ROUTINE)PipeBufferInThread, 621 | (LPVOID)&bt, 622 | 0, NULL); 623 | print("Created in thread %#lx\n", hIn); 624 | 625 | hOut = CreateThread(NULL, 0, 626 | (LPTHREAD_START_ROUTINE)PipeBufferOutThread, 627 | (LPVOID)&bt, 628 | 0, NULL); 629 | print("Created out thread %#lx\n", hOut); 630 | 631 | if (hIn == NULL || hOut == NULL) 632 | { 633 | print("Failed to create threads: %s\n", GetErrorMessage()); 634 | if (!RunningAsService) 635 | { 636 | MessageBox(NULL, GetErrorMessage(), 637 | "Failed to create threads", 638 | MB_OK | MB_ICONSTOP); 639 | } 640 | ExitProcess(1); 641 | } 642 | 643 | print("Waiting for threads to exit\n"); 644 | WaitForSingleObject(hOut, INFINITE); 645 | print("Buffer out thread exited\n"); 646 | 647 | if (RetryNewConnection) 648 | { 649 | RetryNewConnection = FALSE; 650 | print("Retrying new connection\n"); 651 | if (!TerminateThread(hIn, 0)) 652 | print("Failed to terminate thread: %s\n", 653 | GetErrorMessage()); 654 | 655 | if (!TerminateThread(hOut, 0)) 656 | print("Failed to terminate thread: %s\n", 657 | GetErrorMessage()); 658 | 659 | sys_close(fd); 660 | CloseHandle(hOut); 661 | CloseHandle(hIn); 662 | CloseHandle(hPipe); 663 | Sleep(1000); 664 | goto NewConnection; 665 | } 666 | 667 | WaitForSingleObject(hIn, INFINITE); 668 | print("Buffer in thread exited\n"); 669 | CloseHandle(hPipe); 670 | } 671 | -------------------------------------------------------------------------------- /bridge.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/bridge.ico -------------------------------------------------------------------------------- /bridge.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | Simple bridge that allows you to use Discord Rich Presence with Wine games/software. 10 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /bridge.rc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | VS_VERSION_INFO VERSIONINFO 8 | FILEVERSION VER_VERSION 9 | PRODUCTVERSION VER_VERSION 10 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 11 | FILEOS VOS_NT_WINDOWS32 12 | FILETYPE VFT_APP 13 | FILESUBTYPE VFT2_UNKNOWN 14 | BEGIN 15 | BLOCK "StringFileInfo" 16 | BEGIN 17 | BLOCK "040904E4" 18 | BEGIN 19 | VALUE "FileDescription", "Simple bridge that allows you to use Discord Rich Presence with Wine games/software." 20 | VALUE "FileVersion", VER_VERSION_STR 21 | VALUE "InternalName", "bridge" 22 | VALUE "LegalCopyright", "Copyright (c) 2025 EnderIce2" 23 | VALUE "OriginalFilename", "bridge.exe" 24 | VALUE "ProductName", "rpc-bridge" 25 | VALUE "ProductVersion", VER_VERSION_STR 26 | END 27 | END 28 | 29 | BLOCK "VarFileInfo" 30 | BEGIN 31 | VALUE "Translation", 0x409, 1252 32 | END 33 | END 34 | 35 | IDR_MAINMENU MENU 36 | BEGIN 37 | POPUP "&View" 38 | BEGIN 39 | MENUITEM "&Log", IDM_VIEW_LOG 40 | END 41 | POPUP "&Help" 42 | BEGIN 43 | MENUITEM "&Documentation", IDM_HELP_DOCUMENTATION 44 | MENUITEM "&License", IDM_HELP_LICENSE 45 | MENUITEM "&About", IDM_HELP_ABOUT 46 | END 47 | END 48 | 49 | IDR_LICENSE_TXT RCDATA "LICENSE" 50 | 51 | IDI_ICON_128 ICON "bridge.ico" 52 | 53 | CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST bridge.manifest 54 | -------------------------------------------------------------------------------- /build/bridge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script is used to run Steam Play with the bridge. 4 | # Usage: /path/to/bridge.sh %command% 5 | # Original script: https://github.com/0e4ef622/wine-discord-ipc-bridge/blob/master/winediscordipcbridge-steam.sh 6 | # As requested by https://github.com/EnderIce2/rpc-bridge/issues/2 7 | 8 | # Exporting BRIDGE_PATH to provide the bridge with its location. 9 | export BRIDGE_PATH="$(dirname "$0")/bridge.exe" 10 | 11 | # The "--steam" option prevents the game from 12 | # hanging as "running" in Steam after it is closed. 13 | # This is done by creating a dummy service with 14 | # startup type SERVICE_DEMAND_START so this service 15 | # is only started when we use this script. 16 | BRIDGE_CMD="$BRIDGE_PATH --steam" 17 | 18 | # Linux 19 | TEMP_PATH="$XDG_RUNTIME_DIR" 20 | # macOS but Steam Play is not supported on macOS https://github.com/ValveSoftware/Proton/issues/1344 21 | TEMP_PATH=${TEMP_PATH:-"$TMPDIR"} 22 | 23 | VESSEL_PATH="$BRIDGE_PATH" 24 | IPC_PATHS="$TEMP_PATH /run/user/$UID $TEMP_PATH/app/com.discordapp.Discord $TEMP_PATH/.flatpak/dev.vencord.Vesktop/xdg-run $TEMP_PATH/snap.discord $TEMP_PATH/snap.discord-canary" 25 | for discord_ipc in $IPC_PATHS; do 26 | if [ -S "$discord_ipc"/discord-ipc-? ]; then 27 | VESSEL_PATH="$BRIDGE_PATH:$(echo "$discord_ipc"/discord-ipc-?)" 28 | break 29 | fi 30 | done 31 | 32 | PROTON_REMOTE_DEBUG_CMD="$BRIDGE_CMD" PRESSURE_VESSEL_FILESYSTEMS_RW="$VESSEL_PATH:$PRESSURE_VESSEL_FILESYSTEMS_RW" "$@" 33 | -------------------------------------------------------------------------------- /build/launchd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is used to create a LaunchAgent on MacOS, to support the service functionality. 4 | # Usage: ./launchd.sh (install|remove) 5 | 6 | SYMLINK=/tmp/rpc-bridge/tmpdir 7 | LOCATION=~/Library/Application\ Support/rpc-bridge 8 | SCRIPT=$LOCATION/rpc-bridge 9 | AGENT=~/Library/LaunchAgents/com.enderice2.rpc-bridge.plist 10 | 11 | function is_installed() { 12 | if [ -f "$AGENT" ]; then 13 | launchctl list | grep -q "com.enderice2.rpc-bridge" 14 | if [ $? -eq 0 ]; then 15 | return 0 16 | fi 17 | fi 18 | return 1 19 | } 20 | 21 | function install() { 22 | # Directories 23 | if [ ! -d "$SYMLINK" ]; then 24 | mkdir -p "$SYMLINK" 25 | fi 26 | if [ ! -d "$LOCATION" ]; then 27 | mkdir -p "$LOCATION" 28 | fi 29 | 30 | # Link script 31 | if [ -f "$SCRIPT" ]; then 32 | rm -f "$SCRIPT" 33 | fi 34 | echo "#!/bin/bash 35 | TARGET_DIR=/tmp/rpc-bridge/tmpdir 36 | if [ ! -d "\$TARGET_DIR" ]; then 37 | mkdir -p "\$TARGET_DIR" 38 | fi 39 | rm -rf "\$TARGET_DIR" 40 | ln -s "\$TMPDIR" "\$TARGET_DIR"" > "$SCRIPT" 41 | chmod +x "$SCRIPT" 42 | 43 | # LaunchAgent 44 | if [ -f "$AGENT" ]; then 45 | rm -f "$AGENT" 46 | fi 47 | echo " 48 | 49 | 50 | 51 | Label 52 | com.enderice2.rpc-bridge 53 | ProgramArguments 54 | 55 | $SCRIPT 56 | 57 | RunAtLoad 58 | 59 | 60 | " > "$AGENT" 61 | launchctl load "$AGENT" 62 | echo "LaunchAgent has been installed." 63 | } 64 | 65 | function remove() { 66 | rm -f "$SYMLINK" 67 | rm -f "$SCRIPT" 68 | rmdir "$LOCATION" 69 | if [ -f "$AGENT" ]; then 70 | launchctl unload "$AGENT" 71 | fi 72 | rm -f "$AGENT" 73 | echo "LaunchAgent has been removed." 74 | } 75 | 76 | # CLI 77 | if [ $# -eq 0 ]; then 78 | echo "Usage: $0 (install|remove)" 79 | exit 1 80 | fi 81 | 82 | case $1 in 83 | install) 84 | if is_installed; then 85 | echo "LaunchAgent is already installed." 86 | exit 0 87 | fi 88 | install 89 | ;; 90 | remove) 91 | if ! is_installed; then 92 | echo "LaunchAgent is not installed. Continuing anyway." 93 | fi 94 | remove 95 | ;; 96 | *) 97 | echo "Invalid argument. Please use 'install' or 'remove'." 98 | exit 1 99 | ;; 100 | esac 101 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | > **Note:** This documentation is built and deployed using [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) (with mkdocs-video) via GitHub Actions. For the best experience, view it on the [published site](https://enderice2.github.io/rpc-bridge/) or with MkDocs locally. Some features (such as tabs, videos, or special formatting) may not display correctly in plain Markdown viewers. 4 | 5 | This repository contains the documentation for the project. It is written in Markdown and rendered using MkDocs. 6 | 7 | ## Getting Started 8 | 9 | To view the documentation locally, install the required Python packages: 10 | 11 | ```bash 12 | pip install mkdocs mkdocs-material mkdocs-video 13 | ``` 14 | 15 | Once installed, you can serve the documentation locally by running: 16 | 17 | ```bash 18 | mkdocs serve 19 | ``` 20 | 21 | This will start a local web server and you can view the documentation in your browser at `http://127.0.0.1:8000`. 22 | 23 | ## Contributing 24 | 25 | If you want to contribute to the documentation, please follow these steps: 26 | 27 | 1. Fork the repository. 28 | 2. Make your changes in a branch. 29 | 3. Submit a pull request. 30 | 31 | Please ensure that your changes are consistent with the existing style and structure of the documentation. 32 | 33 | ## License 34 | 35 | This documentation is licensed under the MIT License. See the LICENSE file for more information. 36 | -------------------------------------------------------------------------------- /docs/assets/contentwarning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/contentwarning.png -------------------------------------------------------------------------------- /docs/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/favicon.png -------------------------------------------------------------------------------- /docs/assets/flatseal_permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/flatseal_permission.png -------------------------------------------------------------------------------- /docs/assets/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/gui.png -------------------------------------------------------------------------------- /docs/assets/hades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/hades.png -------------------------------------------------------------------------------- /docs/assets/lethalcompany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/lethalcompany.png -------------------------------------------------------------------------------- /docs/assets/lutris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/lutris.png -------------------------------------------------------------------------------- /docs/assets/lutris_lol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/lutris_lol.png -------------------------------------------------------------------------------- /docs/assets/macos-crossover.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/macos-crossover.webm -------------------------------------------------------------------------------- /docs/assets/steam_amongus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/steam_amongus.png -------------------------------------------------------------------------------- /docs/assets/steam_protontricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/steam_protontricks.png -------------------------------------------------------------------------------- /docs/assets/steam_script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/steam_script.png -------------------------------------------------------------------------------- /docs/assets/vividstasis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnderIce2/rpc-bridge/c1078b5b8b1baac536f4a8cb784fb7c1073df616/docs/assets/vividstasis.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Discord RPC Bridge for Wine 2 | 3 | ![GitHub License](https://img.shields.io/github/license/EnderIce2/rpc-bridge?style=for-the-badge) 4 | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/EnderIce2/rpc-bridge/total?style=for-the-badge) 5 | ![GitHub Release](https://img.shields.io/github/v/release/EnderIce2/rpc-bridge?style=for-the-badge) 6 | ![GitHub Pre-Release](https://img.shields.io/github/v/release/EnderIce2/rpc-bridge?include_prereleases&style=for-the-badge&label=pre-release) 7 | 8 | Simple bridge that allows you to use Discord Rich Presence with Wine games/software on Linux/macOS. 9 | 10 | [Download latest release](https://github.com/EnderIce2/rpc-bridge/releases/latest/download/bridge.zip "Recommended"){ .md-button .md-button--primary } 11 | [Download latest pre-release](https://github.com/EnderIce2/rpc-bridge/releases "Unstable builds with experimental features"){ .md-button } 12 | [Download latest build](https://github.com/EnderIce2/rpc-bridge/actions/workflows/build.yml "Builds from the latest commits, here be dragons!"){ .md-button } 13 | 14 | Works by running a small program in the background that creates a [named pipe](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) `\\.\pipe\discord-ipc-0` inside the prefix and forwards all data to the pipe `/run/user/1000/discord-ipc-0`. 15 | 16 | This bridge takes advantage of the Windows service implementation in Wine, eliminating the need to run it manually. 17 | 18 | These docs are for the latest stable release. 19 | For v1.0, see [the original README](https://github.com/EnderIce2/rpc-bridge/blob/v1.0/README.md). 20 | 21 | --- 22 | 23 | ## Known Issues 24 | 25 | - If you use **Vesktop** 26 | Some games may not show up in Discord. This is because Vesktop uses arRPC, which it doesn't work with some games [#4](https://github.com/EnderIce2/rpc-bridge/issues/4#issuecomment-2143549407). This is not an issue with the bridge. 27 | 28 | --- 29 | 30 | ## My game is not showing up in Discord 31 | 32 | If your game is not showing up in Discord, please check the following: 33 | 34 | - The game you are playing has [Rich Presence](https://discord.com/developers/docs/rich-presence/overview) support! 35 | - Some games may not have this feature. It's up to developers of the game to implement it. 36 | This is not an issue related to the bridge. 37 | 38 | - You followed the installation steps correctly. 39 | 40 | - You are using the latest version of the bridge. Currently is ![GitHub Release](https://img.shields.io/github/v/release/EnderIce2/rpc-bridge?style=flat-square&label=%20). 41 | 42 | ### I still want to see the game in Discord! 43 | 44 | This is outside the scope of this project, but here are some workarounds: 45 | 46 | - You can manually add the game to Discord by going to `User Settings >` under `Activity Settings` in `Registered Games` tab. [Official Article](https://support.discord.com/hc/en-us/articles/7931156448919-Activity-Status-Recent-Activity#h_01HTJA8QV5ABSA6FY6GEPMA946) 47 | - Tip: You can rename the game to whatever you want. 48 | 49 | --- 50 | 51 | ## Compiling from source 52 | 53 | - Install the `wine`, `gcc-mingw-w64` and `make` packages. 54 | - Open a terminal in the directory that contains this file and run `make`. 55 | - The compiled executable will be located in `build/bridge.exe`. 56 | 57 | --- 58 | 59 | ## Examples 60 | 61 | [**League Of Legends**](https://www.leagueoflegends.com/en-us/) running under Wine using Lutris 62 | ![image](assets/lutris_lol.png){ width="600" } 63 | 64 | [**Among Us**](https://store.steampowered.com/app/945360/Among_Us/) on Steam 65 | ![image](assets/steam_amongus.png){ width="600" } 66 | 67 | [**Content Warning**](https://store.steampowered.com/app/2881650/Content_Warning/) on Steam 68 | ![image](assets/contentwarning.png){ width="600" } 69 | 70 | [**Hades**](https://store.steampowered.com/app/1145360/Hades/) on Steam 71 | ![image](assets/hades.png){ width="600" } 72 | 73 | [**Lethal Company**](https://store.steampowered.com/app/1966720/Lethal_Company/) ([modded](https://thunderstore.io/c/lethal-company/p/mrov/LethalRichPresence/)) on Steam 74 | ![image](assets/lethalcompany.png){ width="600" } 75 | 76 | [**vivid/stasis**](https://store.steampowered.com/app/2093940/vividstasis/) on Steam 77 | ![image](assets/vividstasis.png){ width="600" } 78 | 79 | ## Credits 80 | 81 | This project is inspired by [wine-discord-ipc-bridge](https://github.com/0e4ef622/wine-discord-ipc-bridge). 82 | 83 | --- 84 | -------------------------------------------------------------------------------- /docs/linux.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Installation will copy itself to `C:\windows\bridge.exe` and create a Windows service. 4 | Logs are stored in `C:\windows\logs\bridge.log`. 5 | 6 | ## Installing inside a prefix 7 | 8 | ### Wine (~/.wine) 9 | 10 | - Double click `bridge.exe` and click `Install`. 11 | - ![gui](assets/gui.png "rpc-bridge GUI") 12 | - To remove, the same process can be followed, but click `Remove` instead. 13 | 14 | ### Lutris 15 | 16 | - Click on a game and select `Run EXE inside Wine prefix`. 17 | - ![lutris](assets/lutris.png "Lutris") 18 | - The same process can be followed as in Wine. 19 | 20 | ### Steam 21 | 22 | There are two ways to install the bridge on Steam. 23 | 24 | #### Using bridge.sh[^1] 25 | 26 | This method is recommended because it's easier to manage. 27 | 28 | - Right click on the game and select `Properties`. 29 | - Under `Set Launch Options`, add the following: 30 | - ![bridge.sh](assets/steam_script.png "Set Launch Options to the path of the bridge.sh") 31 | Of course, you need to replace `/path/to/bridge.sh` with the actual path to the script. 32 | 33 | !!! info "Note" 34 | 35 | `bridge.sh` must be in the same directory as `bridge.exe`. 36 | 37 | #### Using Protontricks 38 | 39 | - Open [Protontricks](https://github.com/Matoking/protontricks) and select the game you want to install the bridge to. 40 | - Select `Select the default wineprefix` 41 | - Select `Browse files` and copy contents of `build` to the game's prefix `drive_c` 42 | - Select `Run a Wine cmd shell` and run `C:\> install.bat` 43 | - If you are not in `C:\`, type `c:` and press enter 44 | 45 | ![protontricks](assets/steam_protontricks.png "If use have the option for 'Run an arbitrary executable (.exe/.msi/.msu), use it instead!") 46 | 47 | !!! warning "If you use Flatpak" 48 | 49 | If you are running Steam, Lutris, etc in a Flatpak, you will need to allow the bridge to access the `/run/user/1000/discord-ipc-0` file. 50 | 51 | You can do this by using [Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) or the terminal. 52 | 53 | === "Flatseal" 54 | 55 | Add `xdg-run/discord-ipc-0` under `Filesystems` category 56 | ![flatseal](assets/flatseal_permission.png) 57 | 58 | === "Terminal" 59 | 60 | - Per application 61 | - `flatpak override --filesystem=xdg-run/discord-ipc-0 ` 62 | - Globally 63 | - `flatpak override --user --filesystem=xdg-run/discord-ipc-0` 64 | 65 | ## Run without installing the service 66 | 67 | If you prefer not to use the service, you can manually run `bridge.exe` within the Wine prefix. 68 | This method is compatible with both Wine and Lutris. 69 | 70 | In Lutris, you can achieve this by adding the path to `bridge.exe` in the `Executable` field under `Game options`. In `Arguments` field, be sure to include the _Windows_ path to the game's executable. 71 | 72 | === "Without bridge" 73 | 74 | - Executable 75 | - `/mnt/games/lutris/league-of-legends/drive_c/Riot Games/League of Legends/LeagueClient.exe` 76 | - Arguments 77 | - `--locale=en_US --launch-product=league_of_legends --launch-patchline=live` 78 | 79 | === "With bridge" 80 | 81 | - Executable 82 | - `/mnt/games/lutris/league-of-legends/drive_c/bridge.exe` 83 | - Arguments 84 | - `"C:\Riot Games\League of Legends\LeagueClient.exe" --locale=en_US --launch-product=league_of_legends --launch-patchline=live` 85 | 86 | In Wine, all you need to do is run `bridge.exe` and select `Start`. 87 | 88 | [^1]: As requested [here](https://github.com/EnderIce2/rpc-bridge/issues/2). 89 | -------------------------------------------------------------------------------- /docs/macos.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Installation will copy itself to `C:\windows\bridge.exe` and create a Windows service. 4 | Logs are stored in `C:\windows\logs\bridge.log`. 5 | 6 | ## Preparing macOS for Installation 7 | 8 | Before proceeding with the installation, you need to set up a **LaunchAgent** due to the way `$TMPDIR` works on macOS. 9 | 10 | - Download the latest build from the [releases](https://github.com/EnderIce2/rpc-bridge/releases). 11 | - Open the archive and make the `launchd.sh` script executable by doing: `chmod +x launchd.sh`. 12 | - To **install** the LaunchAgent, run `./launchd.sh install` and to **remove** it simply run `./launchd.sh remove`. 13 | 14 | The script will add a LaunchAgent to your user, that will symlink the `$TMPDIR` directory to `/tmp/rpc-bridge/tmpdir`. 15 | 16 | ## Video Tutorial on how to install the LaunchAgent + bridge inside CrossOver 17 | 18 | ![type:video](assets/macos-crossover.webm){: style='width: 66%; height: 20vw;'} 19 | 20 | ## Wine (~/.wine) 21 | 22 | - Double click `bridge.exe` and click `Install`. 23 | - ![gui](assets/gui.png "rpc-bridge GUI") 24 | - To remove, the same process can be followed, but click `Remove` instead. 25 | 26 | ## Run without installing the service 27 | 28 | If you prefer not to use the service, you can manually run `bridge.exe` within the prefix, and click on `Start` in the GUI. 29 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## GUI 4 | 5 | - When running the program manually without providing any arguments it will show a GUI. 6 | ![gui](assets/gui.png "rpc-bridge GUI") 7 | - `Start` will start the service without installing itself. 8 | - `Install` will install the service. 9 | - `Remove` will uninstall the service. 10 | 11 | ## CLI 12 | 13 | - `--help` Show help message 14 | - This will show the help message 15 | 16 | - `--version` Show version 17 | - This will show the version of the program 18 | 19 | - `--install` Install the service 20 | - This will copy the binary to `C:\windows\bridge.exe` and register it as a service 21 | 22 | - `--uninstall` Uninstall the service 23 | - This will remove the service and delete `C:\windows\bridge.exe` 24 | 25 | - `--steam` Reserved for Steam 26 | - This will start the service and exit (used with `bridge.sh`) 27 | 28 | - `--no-service` Do not run as service 29 | - (only for `--steam`) 30 | 31 | - `--service` Reserved for service 32 | - Reserved 33 | 34 | - `--rpc ` Set RPC_PATH environment variable 35 | - This is used to specify the directory where `discord-ipc-0` is located -------------------------------------------------------------------------------- /game.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | void print(char const *fmt, ...); 9 | LPTSTR GetErrorMessage(); 10 | 11 | BOOL IsChildProcess(DWORD parentID, DWORD childID) 12 | { 13 | HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 14 | PROCESSENTRY32 pe32; 15 | 16 | if (hSnapshot == INVALID_HANDLE_VALUE) 17 | return FALSE; 18 | 19 | pe32.dwSize = sizeof(PROCESSENTRY32); 20 | 21 | if (Process32First(hSnapshot, &pe32)) 22 | { 23 | do 24 | { 25 | if (pe32.th32ParentProcessID == parentID && pe32.th32ProcessID == childID) 26 | { 27 | CloseHandle(hSnapshot); 28 | return TRUE; 29 | } 30 | } while (Process32Next(hSnapshot, &pe32)); 31 | } 32 | 33 | CloseHandle(hSnapshot); 34 | return FALSE; 35 | } 36 | 37 | BOOL FileExists(LPCTSTR szPath) 38 | { 39 | DWORD dwAttrib = GetFileAttributes(szPath); 40 | return (dwAttrib != INVALID_FILE_ATTRIBUTES && 41 | !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY)); 42 | } 43 | 44 | void LaunchGame(int argc, char **argv) 45 | { 46 | char *gamePath = ""; 47 | int startArg = 1; 48 | if (argc > 1) 49 | { 50 | print("Checking if %s is a valid file\n", argv[1]); 51 | if (FileExists(argv[1])) 52 | { 53 | print("Checking if %s is a valid executable\n", argv[1]); 54 | DWORD dwBinaryType; 55 | if (!GetBinaryType(argv[1], &dwBinaryType)) 56 | { 57 | MessageBox(NULL, GetErrorMessage(), 58 | "GetBinaryType", 59 | MB_OK | MB_ICONSTOP); 60 | ExitProcess(1); 61 | } 62 | print("Executable type: %d\n", dwBinaryType); 63 | 64 | gamePath = argv[1]; 65 | startArg = 2; 66 | } 67 | else 68 | { 69 | MessageBox(NULL, "Invalid game path specified", 70 | NULL, MB_OK | MB_ICONSTOP); 71 | print("%s is not a valid file\n", argv[1]); 72 | ExitProcess(1); 73 | } 74 | } 75 | else if (argc == 1) 76 | { 77 | print("No game path specified. Idling...\n"); 78 | while (TRUE) 79 | Sleep(1000); 80 | } 81 | else 82 | { 83 | MessageBox(NULL, "No game path specified", 84 | NULL, MB_OK | MB_ICONSTOP); 85 | ExitProcess(1); 86 | } 87 | 88 | STARTUPINFO game_si; 89 | PROCESS_INFORMATION game_pi; 90 | 91 | ZeroMemory(&game_si, sizeof(STARTUPINFO)); 92 | game_si.cb = sizeof(STARTUPINFO); 93 | game_si.hStdOutput = INVALID_HANDLE_VALUE; 94 | game_si.hStdError = INVALID_HANDLE_VALUE; 95 | game_si.dwFlags |= STARTF_USESTDHANDLES; 96 | 97 | char *gameArgs = LocalAlloc(LPTR, 512); 98 | for (int i = startArg; i < argc; i++) 99 | { 100 | assert(strlen(gameArgs) + strlen(argv[i]) < 512); 101 | gameArgs = strcat(gameArgs, argv[i]); 102 | gameArgs = strcat(gameArgs, " "); 103 | } 104 | print("Launching \"%s\" with arguments \"%s\"\n", gamePath, gameArgs); 105 | if (!CreateProcess(gamePath, gameArgs, NULL, NULL, FALSE, 106 | 0, NULL, NULL, &game_si, &game_pi)) 107 | { 108 | MessageBox(NULL, GetErrorMessage(), 109 | "CreateProcess", MB_OK | MB_ICONSTOP); 110 | ExitProcess(1); 111 | } 112 | LocalFree(gameArgs); 113 | DWORD parentID = game_pi.dwProcessId; 114 | print("Waiting for PID %d to exit...\n", game_pi.dwProcessId); 115 | WaitForSingleObject(game_pi.hProcess, INFINITE); 116 | print("PID %d exited\n", game_pi.dwProcessId); 117 | 118 | HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 119 | PROCESSENTRY32 pe32; 120 | 121 | if (hSnapshot != INVALID_HANDLE_VALUE) 122 | { 123 | pe32.dwSize = sizeof(PROCESSENTRY32); 124 | if (Process32First(hSnapshot, &pe32)) 125 | { 126 | do 127 | { 128 | if (IsChildProcess(parentID, pe32.th32ProcessID)) 129 | { 130 | WaitForSingleObject(OpenProcess(SYNCHRONIZE, FALSE, 131 | pe32.th32ProcessID), 132 | INFINITE); 133 | print("Waiting for PID %d\n", pe32.th32ProcessID); 134 | } 135 | } while (Process32Next(hSnapshot, &pe32)); 136 | } 137 | CloseHandle(hSnapshot); 138 | } 139 | else 140 | { 141 | MessageBox(NULL, GetErrorMessage(), 142 | "CreateToolhelp32Snapshot", MB_OK | MB_ICONSTOP); 143 | ExitProcess(0); 144 | } 145 | 146 | CloseHandle(game_pi.hProcess); 147 | print("Game exited\n"); 148 | } 149 | -------------------------------------------------------------------------------- /gui.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "resource.h" 8 | 9 | /** 10 | * The entire code could be better written, but at least it works. 11 | * 12 | * This will make installation and removal of the bridge WAY easier. 13 | */ 14 | 15 | LPTSTR GetErrorMessage(); 16 | void print(char const *fmt, ...); 17 | void InstallService(int ServiceStartType, LPCSTR Path); 18 | void RemoveService(); 19 | void CreateBridge(); 20 | extern BOOL IsLinux; 21 | 22 | HWND hwnd = NULL; 23 | HANDLE hBridge = NULL; 24 | extern HANDLE hOut; 25 | extern HANDLE hIn; 26 | 27 | BOOL IsAlreadyRunning = FALSE; 28 | VOID HandleStartButton(BOOL Silent) 29 | { 30 | if (IsAlreadyRunning) 31 | { 32 | HWND item = GetDlgItem(hwnd, 4); 33 | SetWindowText(item, "Do you want to start, install or remove the bridge?"); 34 | RedrawWindow(item, NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); 35 | item = GetDlgItem(hwnd, /* Start Button */ 1); 36 | Button_SetText(item, "&Start"); 37 | EnableWindow(item, FALSE); 38 | RedrawWindow(item, NULL, NULL, RDW_ERASE | RDW_INVALIDATE | RDW_FRAME | RDW_ALLCHILDREN); 39 | 40 | print("Killing %#x, %#lx and waiting for %#lx\n", hIn, hOut, hBridge); 41 | if (hIn != NULL) 42 | TerminateThread(hIn, 0); 43 | if (hOut != NULL) 44 | TerminateThread(hOut, 0); 45 | WaitForSingleObject(hBridge, INFINITE); 46 | 47 | EnableWindow(item, TRUE); 48 | IsAlreadyRunning = FALSE; 49 | return; 50 | } 51 | 52 | SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); 53 | if (hSCManager == NULL) 54 | { 55 | print("OpenSCManager failed: %s\n", GetErrorMessage()); 56 | return; 57 | } 58 | 59 | SC_HANDLE schService = OpenService(hSCManager, "rpc-bridge", 60 | SERVICE_QUERY_CONFIG | SERVICE_CHANGE_CONFIG | SERVICE_START); 61 | if (schService == NULL) 62 | { 63 | print("Service doesn't exist: %s\n", GetErrorMessage()); 64 | 65 | /* Service doesn't exist; running without any service */ 66 | 67 | hBridge = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CreateBridge, 68 | NULL, 0, NULL); 69 | 70 | HWND item = GetDlgItem(hwnd, /* Start Button */ 1); 71 | Button_SetText(item, "&Stop"); 72 | item = GetDlgItem(hwnd, 4); 73 | SetWindowText(item, "Bridge is running..."); 74 | IsAlreadyRunning = TRUE; 75 | ShowWindow(hwnd, SW_MINIMIZE); 76 | return; 77 | } 78 | 79 | DWORD dwBytesNeeded; 80 | QueryServiceConfig(schService, NULL, 0, &dwBytesNeeded); 81 | LPQUERY_SERVICE_CONFIG lpqsc = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LPTR, dwBytesNeeded); 82 | if (lpqsc == NULL) 83 | { 84 | print("LocalAlloc failed: %s\n", GetErrorMessage()); 85 | return; 86 | } 87 | 88 | if (!QueryServiceConfig(schService, lpqsc, dwBytesNeeded, &dwBytesNeeded)) 89 | { 90 | print("QueryServiceConfig failed: %s\n", GetErrorMessage()); 91 | return; 92 | } 93 | 94 | if (StartService(schService, 0, NULL) == FALSE) 95 | { 96 | if (GetLastError() == ERROR_SERVICE_ALREADY_RUNNING) 97 | return; 98 | print("StartService failed: %s\n", GetErrorMessage()); 99 | } 100 | 101 | LocalFree(lpqsc); 102 | CloseServiceHandle(schService); 103 | CloseServiceHandle(hSCManager); 104 | if (Silent == FALSE) 105 | MessageBox(NULL, "Bridge service started successfully", "Info", MB_OK); 106 | print("Bridge service started successfully\n"); 107 | } 108 | 109 | VOID HandleInstallButton() 110 | { 111 | char filename[MAX_PATH]; 112 | GetModuleFileName(NULL, filename, MAX_PATH); 113 | CopyFile(filename, "C:\\windows\\bridge.exe", FALSE); 114 | InstallService(SERVICE_AUTO_START, "C:\\windows\\bridge.exe --service"); 115 | MessageBox(NULL, "Bridge installed successfully", "Info", MB_OK); 116 | HandleStartButton(TRUE); 117 | ExitProcess(0); 118 | } 119 | 120 | VOID HandleRemoveButton() 121 | { 122 | RemoveService(); 123 | MessageBox(NULL, "Bridge removed successfully", "Info", MB_OK); 124 | ExitProcess(0); 125 | } 126 | 127 | void ShowLicenseDialog() 128 | { 129 | HMODULE hModule = GetModuleHandle(NULL); 130 | HRSRC hRes = FindResource(hModule, MAKEINTRESOURCE(IDR_LICENSE_TXT), RT_RCDATA); 131 | if (!hRes) 132 | { 133 | MessageBox(NULL, "Resource not found", "Error", MB_OK | MB_ICONERROR); 134 | return; 135 | } 136 | 137 | HGLOBAL hResData = LoadResource(NULL, hRes); 138 | if (!hResData) 139 | { 140 | MessageBox(NULL, "Resource failed to load", "Error", MB_OK | MB_ICONERROR); 141 | return; 142 | } 143 | 144 | DWORD resSize = SizeofResource(NULL, hRes); 145 | void *pRes = LockResource(hResData); 146 | if (!pRes) 147 | { 148 | MessageBox(NULL, "Resource failed to lock", "Error", MB_OK | MB_ICONERROR); 149 | return; 150 | } 151 | 152 | char *licenseText = (char *)malloc(resSize + 1); 153 | if (!licenseText) 154 | { 155 | MessageBox(NULL, "Memory allocation failed", "Error", MB_OK | MB_ICONERROR); 156 | return; 157 | } 158 | 159 | memcpy(licenseText, pRes, resSize); 160 | licenseText[resSize] = '\0'; 161 | MessageBoxA(hwnd, licenseText, "About", MB_OK); 162 | free(licenseText); 163 | } 164 | 165 | LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 166 | { 167 | switch (msg) 168 | { 169 | case WM_COMMAND: 170 | { 171 | switch (LOWORD(wParam)) 172 | { 173 | case 1: 174 | HandleStartButton(FALSE); 175 | break; 176 | case 2: 177 | HandleInstallButton(); 178 | break; 179 | case 3: 180 | HandleRemoveButton(); 181 | break; 182 | case IDM_VIEW_LOG: 183 | ShellExecute(NULL, "open", "C:\\windows\\notepad.exe", "C:\\windows\\logs\\bridge.log", NULL, SW_SHOW); 184 | break; 185 | case IDM_HELP_DOCUMENTATION: 186 | ShellExecute(NULL, "open", "https://enderice2.github.io/rpc-bridge/index.html", NULL, NULL, SW_SHOWNORMAL); 187 | break; 188 | case IDM_HELP_LICENSE: 189 | ShowLicenseDialog(); 190 | break; 191 | case IDM_HELP_ABOUT: 192 | { 193 | char msg[256]; 194 | snprintf(msg, sizeof(msg), 195 | "rpc-bridge v%s\n" 196 | " branch: %s\n" 197 | " commit: %s\n\n" 198 | "Simple bridge that allows you to use Discord Rich Presence with Wine games/software.\n\n" 199 | "Created by EnderIce2\n\n" 200 | "Licensed under the MIT License", 201 | VER_VERSION_STR, GIT_BRANCH, GIT_COMMIT); 202 | MessageBox(NULL, msg, "About", MB_OK); 203 | break; 204 | } 205 | default: 206 | break; 207 | } 208 | break; 209 | } 210 | case WM_CLOSE: 211 | DestroyWindow(hwnd); 212 | break; 213 | case WM_DESTROY: 214 | PostQuitMessage(0); 215 | ExitProcess(0); 216 | break; 217 | case WM_CTLCOLORSTATIC: 218 | { 219 | HDC hdcStatic = (HDC)wParam; 220 | SetBkMode(hdcStatic, TRANSPARENT); 221 | return (INT_PTR)(HBRUSH)GetStockObject(NULL_BRUSH); 222 | } 223 | default: 224 | return DefWindowProc(hwnd, msg, wParam, lParam); 225 | } 226 | return 0; 227 | } 228 | 229 | VOID SetButtonStyles(INT *btnStartStyle, INT *btnRemoveStyle, INT *btnInstallStyle) 230 | { 231 | *btnStartStyle = WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP; 232 | *btnRemoveStyle = WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP; 233 | *btnInstallStyle = WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP; 234 | 235 | // if (!IsLinux) 236 | // { 237 | // *btnInstallStyle |= WS_DISABLED; 238 | // *btnRemoveStyle |= WS_DISABLED; 239 | // return; 240 | // } 241 | 242 | SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); 243 | SC_HANDLE schService = OpenService(hSCManager, "rpc-bridge", SERVICE_START | SERVICE_QUERY_STATUS); 244 | 245 | if (schService != NULL) 246 | { 247 | *btnInstallStyle |= WS_DISABLED; 248 | 249 | SERVICE_STATUS_PROCESS ssStatus; 250 | DWORD dwBytesNeeded; 251 | assert(QueryServiceStatusEx(schService, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssStatus, 252 | sizeof(SERVICE_STATUS_PROCESS), &dwBytesNeeded)); 253 | 254 | if (ssStatus.dwCurrentState == SERVICE_RUNNING || 255 | ssStatus.dwCurrentState == SERVICE_START_PENDING) 256 | *btnStartStyle |= WS_DISABLED; 257 | } 258 | else 259 | *btnRemoveStyle |= WS_DISABLED; 260 | 261 | CloseServiceHandle(schService); 262 | CloseServiceHandle(hSCManager); 263 | } 264 | 265 | int WINAPI __WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 266 | LPSTR lpCmdLine, int nCmdShow) 267 | { 268 | INT btnStartStyle, btnRemoveStyle, btnInstallStyle; 269 | SetButtonStyles(&btnStartStyle, &btnRemoveStyle, &btnInstallStyle); 270 | 271 | const char szClassName[] = "BridgeWindowClass"; 272 | 273 | WNDCLASSEX wc; 274 | wc.cbSize = sizeof(WNDCLASSEX); 275 | wc.style = 0; 276 | wc.lpfnWndProc = WndProc; 277 | wc.cbClsExtra = 0; 278 | wc.cbWndExtra = 0; 279 | wc.hInstance = hInstance; 280 | wc.hIcon = LoadIcon(NULL, IDI_WINLOGO); 281 | wc.hCursor = LoadCursor(NULL, IDC_ARROW); 282 | wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); 283 | wc.lpszMenuName = NULL; 284 | wc.lpszClassName = szClassName; 285 | wc.hIconSm = LoadIcon(NULL, IDI_WINLOGO); 286 | 287 | assert(RegisterClassEx(&wc)); 288 | 289 | hwnd = CreateWindowEx(WS_EX_WINDOWEDGE, 290 | szClassName, 291 | "Discord RPC Bridge", 292 | WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME, 293 | (GetSystemMetrics(SM_CXSCREEN) - 400) / 2, 294 | (GetSystemMetrics(SM_CYSCREEN) - 150) / 2, 295 | 400, 150, 296 | NULL, NULL, hInstance, NULL); 297 | 298 | HICON hIcon = LoadIcon(hInstance, "IDI_ICON_128"); 299 | SendMessage(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hIcon); 300 | 301 | HWND hLbl4 = CreateWindowEx(WS_EX_TRANSPARENT, 302 | "STATIC", "Do you want to start, install or remove the bridge?", 303 | WS_CHILD | WS_VISIBLE | SS_CENTER, 304 | 0, 15, 400, 25, 305 | hwnd, (HMENU)4, hInstance, NULL); 306 | 307 | HWND hbtn1 = CreateWindow("BUTTON", "&Start", 308 | btnStartStyle, 309 | 40, 60, 100, 30, 310 | hwnd, (HMENU)1, hInstance, NULL); 311 | 312 | HWND hbtn2 = CreateWindow("BUTTON", "&Install", 313 | btnInstallStyle, 314 | 150, 60, 100, 30, 315 | hwnd, (HMENU)2, hInstance, NULL); 316 | 317 | HWND hbtn3 = CreateWindow("BUTTON", "&Remove", 318 | btnRemoveStyle, 319 | 260, 60, 100, 30, 320 | hwnd, (HMENU)3, hInstance, NULL); 321 | 322 | HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MAINMENU)); 323 | SetMenu(hwnd, hMenu); 324 | 325 | HDC hDC = GetDC(hwnd); 326 | int nHeight = -MulDiv(11, GetDeviceCaps(hDC, LOGPIXELSY), 72); 327 | 328 | HFONT hFont = CreateFont(nHeight, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, ANSI_CHARSET, 329 | OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, 330 | DEFAULT_PITCH | FF_DONTCARE, TEXT("Segoe UI")); 331 | ReleaseDC(hwnd, hDC); 332 | 333 | SendMessage(hwnd, WM_SETFONT, hFont, TRUE); 334 | SendMessage(hLbl4, WM_SETFONT, hFont, TRUE); 335 | SendMessage(hbtn1, WM_SETFONT, hFont, TRUE); 336 | SendMessage(hbtn2, WM_SETFONT, hFont, TRUE); 337 | SendMessage(hbtn3, WM_SETFONT, hFont, TRUE); 338 | 339 | ShowWindow(hwnd, nCmdShow); 340 | UpdateWindow(hwnd); 341 | 342 | MSG msg; 343 | while (GetMessage(&msg, NULL, 0, 0) > 0) 344 | { 345 | if (!IsDialogMessage(hwnd, &msg)) 346 | { 347 | TranslateMessage(&msg); 348 | DispatchMessage(&msg); 349 | } 350 | } 351 | return msg.wParam; 352 | } 353 | 354 | void CreateGUI() 355 | { 356 | ShowWindow(GetConsoleWindow(), SW_MINIMIZE); 357 | ExitProcess(__WinMain(GetModuleHandle(NULL), NULL, GetCommandLine(), SW_SHOWNORMAL)); 358 | } 359 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "resource.h" 7 | 8 | FILE *g_logFile = NULL; 9 | BOOL RunningAsService = FALSE; 10 | 11 | void CreateGUI(); 12 | void CreateBridge(); 13 | void LaunchGame(int argc, char **argv); 14 | void ServiceMain(int argc, char *argv[]); 15 | void InstallService(int ServiceStartType, LPCSTR Path); 16 | char *native_getenv(const char *name); 17 | void RemoveService(); 18 | extern BOOL IsLinux; 19 | 20 | LPTSTR GetErrorMessage() 21 | { 22 | DWORD err = GetLastError(); 23 | if (err == 0) 24 | return "Error"; 25 | 26 | WORD wLangID = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); 27 | 28 | LPSTR buffer = NULL; 29 | size_t size = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | 30 | FORMAT_MESSAGE_FROM_SYSTEM | 31 | FORMAT_MESSAGE_IGNORE_INSERTS, 32 | NULL, err, wLangID, (LPSTR)&buffer, 0, NULL); 33 | 34 | LPTSTR message = NULL; 35 | if (size > 0) 36 | { 37 | message = (LPTSTR)LocalAlloc(LPTR, (size + 1) * sizeof(TCHAR)); 38 | if (message != NULL) 39 | { 40 | memcpy(message, buffer, size * sizeof(TCHAR)); 41 | message[size] = '\0'; 42 | } 43 | LocalFree(buffer); 44 | } 45 | 46 | return message; 47 | } 48 | 49 | void DetectWine() 50 | { 51 | HMODULE hNTdll = GetModuleHandle("ntdll.dll"); 52 | if (!hNTdll) 53 | { 54 | MessageBox(NULL, "Failed to load ntdll.dll", 55 | GetErrorMessage(), MB_OK | MB_ICONERROR); 56 | ExitProcess(1); 57 | } 58 | 59 | if (!GetProcAddress(hNTdll, "wine_get_version")) 60 | { 61 | MessageBox(NULL, "This program is only intended to run under Wine.", 62 | "Error", MB_OK | MB_ICONINFORMATION); 63 | ExitProcess(1); 64 | } 65 | 66 | static void(CDECL * wine_get_host_version)(const char **sysname, const char **release); 67 | wine_get_host_version = (void *)GetProcAddress(hNTdll, "wine_get_host_version"); 68 | 69 | assert(wine_get_host_version); 70 | const char *__sysname; 71 | const char *__release; 72 | wine_get_host_version(&__sysname, &__release); 73 | if (strcmp(__sysname, "Linux") != 0 && strcmp(__sysname, "Darwin") != 0) 74 | { 75 | int result = MessageBox(NULL, "This program is designed for Linux and macOS only!\nDo you want to proceed?", 76 | NULL, MB_YESNO | MB_ICONQUESTION); 77 | if (result == IDNO) 78 | ExitProcess(1); 79 | } 80 | 81 | IsLinux = strcmp(__sysname, "Linux") == 0; 82 | } 83 | 84 | void print(char const *fmt, ...) 85 | { 86 | va_list args; 87 | va_start(args, fmt); 88 | vprintf(fmt, args); 89 | va_end(args); 90 | va_start(args, fmt); 91 | vfprintf(g_logFile, fmt, args); 92 | va_end(args); 93 | } 94 | 95 | void HandleArguments(int argc, char *argv[]) 96 | { 97 | if (strcmp(argv[1], "--service") == 0) 98 | { 99 | RunningAsService = TRUE; 100 | print("Running as service\n"); 101 | 102 | SERVICE_TABLE_ENTRY ServiceTable[] = 103 | { 104 | {"rpc-bridge", (LPSERVICE_MAIN_FUNCTION)ServiceMain}, 105 | {NULL, NULL}, 106 | }; 107 | 108 | if (StartServiceCtrlDispatcher(ServiceTable) == FALSE) 109 | { 110 | print("Service failed to start\n"); 111 | ExitProcess(1); 112 | } 113 | } 114 | else if (strcmp(argv[1], "--steam") == 0) 115 | { 116 | /* All this mess just so when you close the game, 117 | it automatically closes the bridge and Steam 118 | will not say that the game is still running. */ 119 | 120 | print("Running as Steam\n"); 121 | if (IsLinux == FALSE) 122 | CreateBridge(); 123 | 124 | if (argc > 2) 125 | { 126 | if (strcmp(argv[2], "--no-service") == 0) 127 | CreateBridge(); 128 | } 129 | 130 | SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); 131 | if (hSCManager == NULL) 132 | { 133 | print("(Steam) OpenSCManager: %s\n", GetErrorMessage()); 134 | ExitProcess(1); 135 | } 136 | 137 | SC_HANDLE schService = OpenService(hSCManager, "rpc-bridge", 138 | SERVICE_QUERY_CONFIG | SERVICE_CHANGE_CONFIG | SERVICE_START); 139 | if (schService == NULL) 140 | { 141 | if (GetLastError() != ERROR_SERVICE_DOES_NOT_EXIST) 142 | { 143 | print("(Steam) OpenService: %s\n", GetErrorMessage()); 144 | ExitProcess(1); 145 | } 146 | 147 | print("(Steam) Service does not exist, registering...\n"); 148 | 149 | WCHAR *(CDECL * wine_get_dos_file_name)(LPCSTR str) = 150 | (void *)GetProcAddress(GetModuleHandleA("KERNEL32"), 151 | "wine_get_dos_file_name"); 152 | 153 | char *unixPath = native_getenv("BRIDGE_PATH"); 154 | if (unixPath == NULL) 155 | { 156 | print("(Steam) BRIDGE_PATH not set\n"); 157 | ExitProcess(1); 158 | } 159 | WCHAR *dosPath = wine_get_dos_file_name(unixPath); 160 | LPSTR asciiPath = (LPSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MAX_PATH); 161 | WideCharToMultiByte(CP_ACP, 0, dosPath, -1, asciiPath, MAX_PATH, NULL, NULL); 162 | 163 | strcat_s(asciiPath, MAX_PATH, " --service"); 164 | print("(Steam) Binary path: %s\n", asciiPath); 165 | 166 | InstallService(SERVICE_DEMAND_START, asciiPath); 167 | HeapFree(GetProcessHeap(), 0, asciiPath); 168 | 169 | /* Create handle for StartService below */ 170 | print("(Steam) Service registered, opening handle...\n"); 171 | /* FIXME: For some reason here it freezes??? */ 172 | schService = OpenService(hSCManager, "rpc-bridge", SERVICE_START); 173 | if (schService == NULL) 174 | { 175 | print("(Steam) Cannot open service after creation: %s\n", GetErrorMessage()); 176 | ExitProcess(1); 177 | } 178 | } 179 | else 180 | { 181 | DWORD dwBytesNeeded; 182 | QueryServiceConfig(schService, NULL, 0, &dwBytesNeeded); 183 | LPQUERY_SERVICE_CONFIG lpqsc = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LPTR, dwBytesNeeded); 184 | if (lpqsc == NULL) 185 | { 186 | print("(Steam) LocalAlloc: %s\n", GetErrorMessage()); 187 | ExitProcess(1); 188 | } 189 | 190 | if (!QueryServiceConfig(schService, lpqsc, dwBytesNeeded, &dwBytesNeeded)) 191 | { 192 | print("(Steam) QueryServiceConfig: %s\n", GetErrorMessage()); 193 | ExitProcess(1); 194 | } 195 | 196 | WCHAR *(CDECL * wine_get_dos_file_name)(LPCSTR str) = 197 | (void *)GetProcAddress(GetModuleHandleA("KERNEL32"), 198 | "wine_get_dos_file_name"); 199 | 200 | char *unixPath = native_getenv("BRIDGE_PATH"); 201 | if (unixPath == NULL) 202 | { 203 | print("(Steam) BRIDGE_PATH not set\n"); 204 | ExitProcess(1); 205 | } 206 | WCHAR *dosPath = wine_get_dos_file_name(unixPath); 207 | LPSTR asciiPath = (LPSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, MAX_PATH); 208 | WideCharToMultiByte(CP_ACP, 0, dosPath, -1, asciiPath, MAX_PATH, NULL, NULL); 209 | 210 | strcat_s(asciiPath, MAX_PATH, " --service"); 211 | print("(Steam) Binary path: %s\n", asciiPath); 212 | 213 | if (strcmp(lpqsc->lpBinaryPathName, asciiPath) != 0) 214 | { 215 | print("(Steam) Service binary path is not correct, updating...\n"); 216 | ChangeServiceConfig(schService, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, 217 | asciiPath, NULL, NULL, NULL, NULL, NULL, NULL); 218 | } 219 | else 220 | print("(Steam) Service binary path is correct\n"); 221 | HeapFree(GetProcessHeap(), 0, asciiPath); 222 | } 223 | 224 | print("(Steam) Starting service and then exiting...\n"); 225 | fclose(g_logFile); 226 | 227 | if (StartService(schService, 0, NULL) != FALSE) 228 | { 229 | CloseServiceHandle(schService); 230 | CloseServiceHandle(hSCManager); 231 | ExitProcess(0); 232 | } 233 | else if (GetLastError() == ERROR_SERVICE_ALREADY_RUNNING) 234 | { 235 | CloseServiceHandle(schService); 236 | CloseServiceHandle(hSCManager); 237 | ExitProcess(0); 238 | } 239 | 240 | MessageBox(NULL, GetErrorMessage(), 241 | "StartService", 242 | MB_OK | MB_ICONSTOP); 243 | ExitProcess(1); 244 | } 245 | else if (strcmp(argv[1], "--install") == 0) 246 | { 247 | char filename[MAX_PATH]; 248 | GetModuleFileName(NULL, filename, MAX_PATH); 249 | CopyFile(filename, "C:\\windows\\bridge.exe", FALSE); 250 | 251 | InstallService(SERVICE_AUTO_START, "C:\\windows\\bridge.exe --service"); 252 | ExitProcess(0); 253 | } 254 | else if (strcmp(argv[1], "--uninstall") == 0) 255 | { 256 | RemoveService(); 257 | ExitProcess(0); 258 | } 259 | else if (strcmp(argv[1], "--rpc") == 0) 260 | { 261 | if (argc < 3) 262 | { 263 | print("No directory provided\n"); 264 | ExitProcess(1); 265 | } 266 | 267 | SetEnvironmentVariable("BRIDGE_RPC_PATH", argv[2]); 268 | print("BRIDGE_RPC_PATH has been set to \"%s\"\n", argv[2]); 269 | CreateBridge(); 270 | ExitProcess(0); 271 | } 272 | else if (strcmp(argv[1], "--version") == 0) 273 | { 274 | /* Already shows the version */ 275 | ExitProcess(0); 276 | } 277 | else if (strcmp(argv[1], "--help") == 0) 278 | { 279 | print("Usage:\n" 280 | " %s [args]\n" 281 | "\n" 282 | "Arguments:\n" 283 | " --help Show this help\n" 284 | "\n" 285 | " --version Show version\n" 286 | "\n" 287 | " --install Install service\n" 288 | " This will copy the binary to C:\\windows\\bridge.exe and register it as a service\n" 289 | "\n" 290 | " --uninstall Uninstall service\n" 291 | " This will remove the service and delete C:\\windows\\bridge.exe\n" 292 | "\n" 293 | " --steam Reserved for Steam\n" 294 | " This will start the service and exit (used with bridge.sh)\n" 295 | "\n" 296 | " --no-service Do not run as service\n" 297 | " (only for --steam)\n" 298 | "\n" 299 | " --service Reserved for service\n" 300 | "\n" 301 | " --rpc Set RPC_PATH environment variable\n" 302 | " This is used to specify the directory where 'discord-ipc-0' is located\n" 303 | "\n" 304 | "Note: If no arguments are provided, the GUI will be shown instead\n", 305 | argv[0]); 306 | ExitProcess(0); 307 | } 308 | } 309 | 310 | int main(int argc, char *argv[]) 311 | { 312 | DetectWine(); 313 | char *logFilePath = "C:\\windows\\logs\\bridge.log"; 314 | g_logFile = fopen(logFilePath, "w"); 315 | if (g_logFile == NULL) 316 | { 317 | MessageBox(NULL, "Failed to open logs file", "Error", MB_OK | MB_ICONERROR); 318 | printf("Failed to open logs file: %ld\n", GetLastError()); 319 | ExitProcess(1); 320 | } 321 | 322 | print("rpc-bridge v%s %s-%s\n", VER_VERSION_STR, GIT_BRANCH, GIT_COMMIT); 323 | if (argc > 1) 324 | HandleArguments(argc, argv); 325 | else 326 | CreateGUI(); 327 | 328 | CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CreateBridge, 329 | NULL, 0, NULL); 330 | Sleep(500); 331 | LaunchGame(argc, argv); 332 | 333 | fclose(g_logFile); 334 | ExitProcess(0); 335 | } 336 | 337 | int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) 338 | { 339 | return main(__argc, __argv); 340 | } 341 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: rpc-bridge 2 | repo_url: https://github.com/EnderIce2/rpc-bridge 3 | repo_name: EnderIce2/rpc-bridge 4 | theme: 5 | name: material 6 | features: 7 | - content.code.copy 8 | - content.tabs.link 9 | - navigation.tabs 10 | - navigation.top 11 | - navigation.footer 12 | - toc.integrate 13 | - content.tooltips 14 | palette: 15 | - media: "(prefers-color-scheme)" 16 | toggle: 17 | icon: material/brightness-auto 18 | name: Switch to dark mode 19 | - media: "(prefers-color-scheme: dark)" 20 | scheme: slate 21 | primary: black 22 | accent: indigo 23 | toggle: 24 | icon: material/brightness-4 25 | name: Switch to light mode 26 | - media: "(prefers-color-scheme: light)" 27 | scheme: default 28 | primary: indigo 29 | accent: indigo 30 | toggle: 31 | icon: material/brightness-7 32 | name: Switch to system preference 33 | font: 34 | text: Roboto 35 | code: Roboto Mono 36 | favicon: assets/favicon.png 37 | logo: assets/favicon.png 38 | icon: 39 | logo: logo 40 | admonition: 41 | note: octicons/tag-16 42 | abstract: octicons/checklist-16 43 | info: octicons/info-16 44 | tip: octicons/squirrel-16 45 | success: octicons/check-16 46 | question: octicons/question-16 47 | warning: octicons/alert-16 48 | failure: octicons/x-circle-16 49 | danger: octicons/zap-16 50 | bug: octicons/bug-16 51 | example: octicons/beaker-16 52 | quote: octicons/quote-16 53 | markdown_extensions: 54 | - pymdownx.highlight: 55 | anchor_linenums: true 56 | line_spans: __span 57 | pygments_lang_class: true 58 | - admonition 59 | - pymdownx.details 60 | - pymdownx.inlinehilite 61 | - pymdownx.snippets 62 | - footnotes 63 | - attr_list 64 | - pymdownx.critic 65 | - pymdownx.caret 66 | - pymdownx.keys 67 | - pymdownx.mark 68 | - pymdownx.tilde 69 | - pymdownx.tabbed: 70 | alternate_style: true 71 | plugins: 72 | - offline 73 | - mkdocs-video: 74 | is_video: true 75 | video_controls: true 76 | video_loop: false 77 | video_muted: false 78 | nav: 79 | - Home: index.md 80 | - Linux: linux.md 81 | - macOS: macos.md 82 | - Usage: usage.md 83 | -------------------------------------------------------------------------------- /resource.h: -------------------------------------------------------------------------------- 1 | #define IDR_MAINMENU 101 2 | #define IDR_LICENSE_TXT 102 3 | #define IDM_HELP_DOCUMENTATION 40001 4 | #define IDM_HELP_LICENSE 40002 5 | #define IDM_HELP_ABOUT 40003 6 | #define IDM_VIEW_LOG 40004 7 | 8 | #define VER_VERSION 1, 4, 0, 0 9 | #define VER_VERSION_STR "1.4.0.0\0" 10 | -------------------------------------------------------------------------------- /service.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | SERVICE_STATUS g_ServiceStatus; 6 | SERVICE_STATUS_HANDLE g_StatusHandle = NULL; 7 | 8 | void print(char const *fmt, ...); 9 | void CreateBridge(); 10 | LPTSTR GetErrorMessage(); 11 | extern BOOL IsLinux; 12 | 13 | void WINAPI ServiceCtrlHandler(DWORD CtrlCode) 14 | { 15 | switch (CtrlCode) 16 | { 17 | case SERVICE_CONTROL_STOP: 18 | case SERVICE_ACCEPT_SHUTDOWN: 19 | { 20 | g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING; 21 | SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 22 | 23 | print("Stopping service\n"); 24 | 25 | /* ... */ 26 | 27 | g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; 28 | SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 29 | break; 30 | } 31 | default: 32 | { 33 | print("Unrecognized service control code %d\n", CtrlCode); 34 | } 35 | } 36 | } 37 | 38 | DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) 39 | { 40 | print("Service started\n"); 41 | CreateBridge(); 42 | return ERROR_SUCCESS; 43 | } 44 | 45 | void ServiceMain(DWORD argc, LPTSTR *argv) 46 | { 47 | print("Starting service\n"); 48 | g_StatusHandle = RegisterServiceCtrlHandler("rpc-bridge", 49 | ServiceCtrlHandler); 50 | if (g_StatusHandle == NULL) 51 | { 52 | print("Failed to register service control handler\n"); 53 | return; 54 | } 55 | 56 | ZeroMemory(&g_ServiceStatus, sizeof(g_ServiceStatus)); 57 | 58 | g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; 59 | g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; 60 | SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 61 | 62 | g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; 63 | g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; 64 | SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 65 | 66 | g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | 67 | SERVICE_ACCEPT_SHUTDOWN; 68 | g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; 69 | g_ServiceStatus.dwWin32ExitCode = 0; 70 | g_ServiceStatus.dwCheckPoint = 0; 71 | SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 72 | 73 | HANDLE hThread = CreateThread(NULL, 0, ServiceWorkerThread, 74 | NULL, 0, NULL); 75 | WaitForSingleObject(hThread, INFINITE); 76 | 77 | g_ServiceStatus.dwControlsAccepted = 0; 78 | g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; 79 | g_ServiceStatus.dwWin32ExitCode = 0; 80 | g_ServiceStatus.dwCheckPoint = 3; 81 | SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 82 | 83 | print("Service stopped.\n"); 84 | return; 85 | } 86 | 87 | void InstallService(int ServiceStartType, LPCSTR Path) 88 | { 89 | print("Registering service\n"); 90 | 91 | // if (IsLinux == FALSE) 92 | // { 93 | // /* FIXME: I don't know how to get the TMPDIR without getenv */ 94 | // MessageBox(NULL, "Registering as a service is not supported on macOS at the moment.", 95 | // "Unsupported", MB_OK | MB_ICONINFORMATION); 96 | // ExitProcess(1); 97 | // } 98 | 99 | SC_HANDLE schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); 100 | if (schSCManager == NULL) 101 | { 102 | print("Failed to open service manager\n"); 103 | MessageBox(NULL, GetErrorMessage(), 104 | "OpenSCManager", 105 | MB_OK | MB_ICONSTOP); 106 | ExitProcess(1); 107 | } 108 | 109 | DWORD dwTagId; 110 | SC_HANDLE schService = CreateService(schSCManager, 111 | "rpc-bridge", "Wine RPC Bridge", 112 | SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, 113 | ServiceStartType, SERVICE_ERROR_NORMAL, 114 | Path, NULL, &dwTagId, NULL, NULL, NULL); 115 | 116 | if (schService == NULL) 117 | { 118 | print("Failed to create service\n"); 119 | MessageBox(NULL, GetErrorMessage(), 120 | "CreateService", 121 | MB_OK | MB_ICONSTOP); 122 | ExitProcess(1); 123 | } 124 | 125 | print("Service installed successfully\n"); 126 | CloseServiceHandle(schService); 127 | CloseServiceHandle(schSCManager); 128 | } 129 | 130 | void RemoveService() 131 | { 132 | print("Unregistering from startup\n"); 133 | 134 | SC_HANDLE schSCManager, schService; 135 | SERVICE_STATUS ssSvcStatus; 136 | 137 | schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); 138 | if (schSCManager == NULL) 139 | { 140 | MessageBox(NULL, GetErrorMessage(), 141 | "OpenSCManager", 142 | MB_OK | MB_ICONSTOP); 143 | ExitProcess(1); 144 | } 145 | 146 | schService = OpenService(schSCManager, "rpc-bridge", 147 | SERVICE_STOP | SERVICE_QUERY_STATUS | DELETE); 148 | 149 | if (schService == NULL) 150 | { 151 | MessageBox(NULL, GetErrorMessage(), 152 | "OpenService", 153 | MB_OK | MB_ICONSTOP); 154 | ExitProcess(1); 155 | } 156 | 157 | if (ControlService(schService, SERVICE_CONTROL_STOP, &ssSvcStatus)) 158 | { 159 | print("Stopping service\n"); 160 | Sleep(1000); 161 | 162 | while (QueryServiceStatus(schService, &ssSvcStatus)) 163 | { 164 | if (ssSvcStatus.dwCurrentState == SERVICE_STOP_PENDING) 165 | { 166 | print("Waiting for service to stop\n"); 167 | Sleep(1000); 168 | } 169 | else 170 | break; 171 | } 172 | 173 | if (ssSvcStatus.dwCurrentState == SERVICE_STOPPED) 174 | print("Service stopped\n"); 175 | else 176 | print("Service failed to stop\n"); 177 | } 178 | 179 | if (!DeleteService(schService)) 180 | { 181 | MessageBox(NULL, GetErrorMessage(), 182 | "DeleteService", 183 | MB_OK | MB_ICONSTOP); 184 | ExitProcess(1); 185 | } 186 | 187 | DeleteFile("C:\\windows\\bridge.exe"); 188 | print("Service removed successfully\n"); 189 | CloseServiceHandle(schService); 190 | CloseServiceHandle(schSCManager); 191 | } 192 | --------------------------------------------------------------------------------