diff --git a/.gitattributes b/.gitattributes index 94f480d..39bd3c3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -* text=auto eol=lf \ No newline at end of file +* text=auto eol=lf +res/unregister.bat text eol=crlf \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 723bab2..b1acb24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,22 +27,18 @@ jobs: uses: lukka/run-vcpkg@v11 with: runVcpkgInstall: true - vcpkgGitCommitId: '03e366fb91e38b9432ebd5f8cc79f7c8f55e96ab' - + vcpkgGitCommitId: '4493042c759d3bdff26164695dbee500d1e696c8' + - name: Integrate vcpkg run: vcpkg integrate install - name: Build Release working-directory: ${{env.GITHUB_WORKSPACE}} - run: | - msbuild /m /p:VcpkgEnableManifest=true /p:Platform=x64 /p:Configuration=Release - copy LICENSE x64/Release/LICENSE.txt + run: msbuild /m /p:VcpkgEnableManifest=true /p:Platform=x64 /p:Configuration=Release - name: Build Debug working-directory: ${{env.GITHUB_WORKSPACE}} - run: | - msbuild /m /p:VcpkgEnableManifest=true /p:Platform=x64 /p:Configuration=Debug - copy LICENSE x64/Debug/LICENSE.txt + run: msbuild /m /p:VcpkgEnableManifest=true /p:Platform=x64 /p:Configuration=Debug - uses: actions/upload-artifact@v7 with: @@ -51,7 +47,10 @@ jobs: x64/Release/SGDBoop.exe x64/Release/libcurl.dll x64/Release/zlib1.dll + x64/Release/z.dll x64/Release/LICENSE.txt + x64/Release/unregister.bat + x64/Release/toggle_debug.bat - uses: actions/upload-artifact@v7 with: @@ -74,22 +73,18 @@ jobs: uses: lukka/run-vcpkg@v11 with: runVcpkgInstall: true - vcpkgGitCommitId: '4b77da7fed37817f124936239197833469f1b9a8' - + vcpkgGitCommitId: '4493042c759d3bdff26164695dbee500d1e696c8' + - name: Integrate vcpkg run: vcpkg integrate install - name: Build Release working-directory: ${{env.GITHUB_WORKSPACE}} - run: | - msbuild /m /p:VcpkgEnableManifest=true /p:Platform=ARM64 /p:Configuration=Release - copy LICENSE ARM64/Release/LICENSE.txt + run: msbuild /m /p:VcpkgEnableManifest=true /p:Platform=ARM64 /p:Configuration=Release - name: Build Debug working-directory: ${{env.GITHUB_WORKSPACE}} - run: | - msbuild /m /p:VcpkgEnableManifest=true /p:Platform=ARM64 /p:Configuration=Debug - copy LICENSE ARM64/Debug/LICENSE.txt + run: msbuild /m /p:VcpkgEnableManifest=true /p:Platform=ARM64 /p:Configuration=Debug - uses: actions/upload-artifact@v7 with: @@ -98,7 +93,9 @@ jobs: ARM64/Release/SGDBoop.exe ARM64/Release/libcurl.dll ARM64/Release/zlib1.dll + ARM64/Release/z.dll ARM64/Release/LICENSE.txt + ARM64/Release/unregister.bat - uses: actions/upload-artifact@v7 with: @@ -120,17 +117,15 @@ jobs: sudo apt-get install -y libcurl4-openssl-dev libgtk-3-dev - name: Build - run: | - sudo make - cp LICENSE linux-release/LICENSE + run: make dist - uses: actions/upload-artifact@v7 with: - name: linux-x64-build - path: linux-release/* + name: linux-x64 + path: dist/* - linux-arm64: - name: "Build Linux arm64" + linux-aarch64: + name: "Build Linux aarch64" runs-on: ubuntu-22.04-arm env: OS: Linux @@ -144,14 +139,31 @@ jobs: sudo apt-get install -y libcurl4-openssl-dev libgtk-3-dev - name: Build + run: make dist + + - uses: actions/upload-artifact@v7 + with: + name: linux-aarch64 + path: dist/* + + macos: + name: "Build MacOS Universal" + runs-on: macos-latest + + steps: + - uses: actions/checkout@v7 + + - name: Install libcurl run: | - sudo make - cp LICENSE linux-release/LICENSE + brew install curl + + - name: Build + run: make dist - uses: actions/upload-artifact@v7 with: - name: linux-arm64-build - path: linux-release/* + name: macos-universal + path: dist/* release: if: github.event_name == 'release' && github.event.action == 'created' @@ -159,7 +171,7 @@ jobs: runs-on: ubuntu-slim permissions: contents: write - needs: [windows-x64, windows-arm64,linux-x64,linux-arm64] + needs: [windows-x64, windows-arm64,linux-x64,linux-aarch64, macos] steps: - uses: actions/download-artifact@v8 with: @@ -170,15 +182,11 @@ jobs: 7z a sgdboop-win64.zip ./artifacts/win-x64-release/* 7z a sgdboop-winarm64.zip ./artifacts/win-arm64-release/* - - name: Archive Linux Release Builds + - name: Get release dists built via "make dist" run: | - chmod +x artifacts/linux-x64-build/install.sh artifacts/linux-x64-build/SGDBoop - cd artifacts/linux-x64-build/ - tar -czvf ../../sgdboop-linux64.tar.gz * - - chmod +x artifacts/linux-arm64-build/install.sh artifacts/linux-arm64-build/SGDBoop - cd artifacts/linux-arm64-build/ - tar -czvf ../../sgdboop-linuxarm64.tar.gz * + mv artifacts/linux-x64/sgdboop-linux-x86_64.tar.gz . + mv artifacts/linux-aarch64/sgdboop-linux-aarch64.tar.gz . + mv artifacts/macos-universal/sgdboop-darwin-universal.zip . - uses: ncipollo/release-action@v1 with: @@ -188,4 +196,4 @@ jobs: replacesArtifacts: true tag: ${{github.event.release.tag_name}} commit: ${{github.sha}} - artifacts: "sgdboop-linux64.tar.gz,sgdboop-linuxarm64.tar.gz,sgdboop-win64.zip,sgdboop-winarm64.zip" + artifacts: "sgdboop-linux-x86_64.tar.gz,sgdboop-linux-aarch64.tar.gz,sgdboop-win64.zip,sgdboop-winarm64.zip,sgdboop-darwin-universal.zip" diff --git a/.gitignore b/.gitignore index 27958af..0c59b6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,403 +1,4 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml - -# Prerequisites +# Prerequisites *.d # Object files @@ -416,10 +17,14 @@ FodyWeavers.xsd *.pch # Libraries +*.lib +*.a *.la *.lo # Shared objects (inc. Windows DLLs) +*.dll +*.so *.so.* *.dylib @@ -446,7 +51,67 @@ Module.symvers Mkfile.old dkms.conf +# debug information files +*.dwo + # VCPKG vcpkg_installed/ -linux-release/SGDBoop +# Visual Studio 2015/2017 cache/options directory +.vs/ + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +dist/ + +./SGDBoop # linux binary + +.DS_Store +/settings.VisualStudio.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9fc3649 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,43 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "SGDBoop (macOS, test message box)", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/SGDBoop.app", + "args": ["sgdb://boop/test"], + "cwd": "${workspaceFolder}", + "preLaunchTask": "build" + }, + { + "name": "SGDBoop (macOS, unregister)", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/SGDBoop.app", + "args": ["unregister"], + "cwd": "${workspaceFolder}", + "preLaunchTask": "build" + }, + { + "name": "SGDBoop (macOS, custom args)", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/SGDBoop.app", + "args": ["${input:sgdboopArgs}"], + "cwd": "${workspaceFolder}", + "preLaunchTask": "build" + } + ], + "inputs": [ + { + "id": "sgdboopArgs", + "type": "promptString", + "description": "Argument to pass to SGDBoop (e.g. sgdb://boop/test, unregister, sgdb://boop/)", + "default": "sgdb://boop/test" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2d2b11d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "make", + "args": ["build"], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": ["$gcc"] + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index f9d890b..76950b3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ zlib License -Copyright (c) 2025 Manos Paterakis, Jozen Blue Martinez +Copyright (c) 2026 Manos Paterakis, Jozen Blue Martinez This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages diff --git a/Makefile b/Makefile index 25a4739..4aeea76 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,84 @@ +# Either "linux" or "darwin", windows is built via mstools atm +OS_NAME := $(shell uname -s | tr A-Z a-z) +ARCH := $(shell uname -m) +VERSION := $(shell awk -F'"' '/define VERSION/{print $$2}' sgdboop.c) + ifeq ($(PREFIX),) PREFIX := /usr/local endif USER_LIB_PATH := /usr/lib -ifdef FLATPAK_ID +ifdef FLATPAK_BUILDER_BUILDDIR USER_LIB_PATH := /app/lib endif PKG_CONFIG ?= pkg-config -STATIC_DIR := linux-release -OUTPUT_DIR ?= linux-release - -.PHONY: all all: build .PHONY: build -build: - $(CC) -O3 -g sgdboop.c curl-helper.c gui-helper-gtk.c string-helpers.c crc.c $(shell $(PKG_CONFIG) --cflags --libs gtk+-3.0 libcurl) -o $(OUTPUT_DIR)/SGDBoop $(LDFLAGS) +build: build-$(OS_NAME) + @echo "Built for target: $(OS_NAME)" + +.PHONY: build-linux build-darwin +build-linux: + $(CC) -O3 -g sgdboop.c curl-helper.c gui-helper-gtk.c string-helpers.c crc.c $(shell $(PKG_CONFIG) --cflags --libs gtk+-3.0 libcurl) -o SGDBoop $(LDFLAGS) + chmod +x SGDBoop -ifdef FLATPAK_ID -.PHONY: install -install: build - install -Dm644 $(STATIC_DIR)/com.steamgriddb.SGDBoop-flatpak.desktop /app/share/applications/com.steamgriddb.SGDBoop.desktop - install -Dm644 com.steamgriddb.SGDBoop.appdata.xml -t /app/share/metainfo - install -Dm755 $(OUTPUT_DIR)/SGDBoop -t /app/bin - install -Dm644 res/com.steamgriddb.SGDBoop.svg -t /app/share/icons/hicolor/scalable/apps +build-darwin: + mkdir -p SGDBoop.app/Contents/MacOS + mkdir -p SGDBoop.app/Contents/Resources + cp LICENSE SGDBoop.app/Contents/Resources/LICENSE.txt + cp res/mac/AppIcon.icns SGDBoop.app/Contents/Resources/AppIcon.icns + cp res/mac/Assets.car SGDBoop.app/Contents/Resources/Assets.car + cp res/mac/Info.plist SGDBoop.app/Contents/Info.plist + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(VERSION)" SGDBoop.app/Contents/Info.plist + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $(VERSION)" SGDBoop.app/Contents/Info.plist + $(CC) -Wno-pointer-sign -fobjc-arc -arch arm64 -arch x86_64 -g sgdboop.c curl-helper.c gui-helper-mac.m string-helpers.c crc.c $(shell $(PKG_CONFIG) --cflags --libs libcurl) -framework Cocoa -o SGDBoop.app/Contents/MacOS/SGDBoop $(LDFLAGS) + chmod +x SGDBoop.app/Contents/MacOS/SGDBoop + +.PHONY: install install-linux install-flatpak +ifdef FLATPAK_BUILDER_BUILDDIR +install: install-flatpak else -.PHONY: install -install: build - install -Dm755 $(OUTPUT_DIR)/SGDBoop -t $(DESTDIR)$(PREFIX)/bin - desktop-file-install $(STATIC_DIR)/com.steamgriddb.SGDBoop.desktop --rebuild-mime-info-cache +install: install-$(OS_NAME) +endif + +install-linux: build + install -Dm755 SGDBoop -t $(DESTDIR)$(PREFIX)/bin + desktop-file-install res/linux/com.steamgriddb.SGDBoop.desktop --rebuild-mime-info-cache @su - $(SUDO_USER) xdg-mime default com.steamgriddb.SGDBoop.desktop x-scheme-handler/sgdb -endif \ No newline at end of file + +install-flatpak: build + install -Dm644 res/linux/com.steamgriddb.SGDBoop-flatpak.desktop /app/share/applications/com.steamgriddb.SGDBoop.desktop + install -Dm644 com.steamgriddb.SGDBoop.appdata.xml -t /app/share/metainfo + install -Dm755 SGDBoop -t /app/bin + install -Dm644 res/com.steamgriddb.SGDBoop.svg -t /app/share/icons/hicolor/scalable/apps + +.PHONY: dist +dist: build + mkdir -p dist/ +# if mac delete symbols before archiving +ifeq ($(OS_NAME),darwin) + rm -r SGDBoop.app/Contents/MacOS/*.dSYM + ditto -c -k --keepParent $(shell $(MAKE) -s print-dists-darwin) dist/sgdboop-$(OS_NAME)-universal.zip +else + mkdir -p dist/$(OS_NAME) + cp $(shell $(MAKE) -s print-dists-$(OS_NAME)) dist/$(OS_NAME)/ + tar -czvf $(CURDIR)/dist/sgdboop-$(OS_NAME)-$(ARCH).tar.gz -C dist/$(OS_NAME) . + rm -rf dist/$(OS_NAME) +endif + +.PHONY: print-dists print-dists-linux print-dists-darwin +print-dists: print-dists-$(OS_NAME) + +print-dists-linux: + @echo "SGDBoop" "LICENSE" "res/linux/com.steamgriddb.SGDBoop.desktop" "res/linux/install.sh" + +print-dists-darwin: + @echo "SGDBoop.app" + +.PHONY: clean +clean: + rm -rf SGDBoop.app SGDBoop dist \ No newline at end of file diff --git a/README.md b/README.md index 6136097..471e010 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,89 @@ # SGDBoop + SGDBoop is a tool that automatically applies assets from [SteamGridDB](https://www.steamgriddb.com/) directly to your Steam library, removing the need to download and set them manually. -# Instructions -## Set up for Windows -- Download the latest [sgdboop-win64.zip](https://github.com/SteamGridDB/SGDBoop/releases/latest/download/sgdboop-win64.zip) -- Extract the program in any directory you prefer -- Run it once with Administrator privileges (can be done via Right Click -> Run as Administrator) +# Setup Instructions + +## Windows + +1. Download the latest [sgdboop-win64.zip](https://github.com/SteamGridDB/SGDBoop/releases/latest/download/sgdboop-win64.zip) +2. Extract the program in any directory you prefer +3. Run it once with Administrator privileges (can be done via Right Click -> Run as Administrator) -In case you want to completely remove the app from your system: +To remove from your system and unregister the URL handler: -- Run the program with the 'unregister' argument (For example, in an Admin CMD run `SGDBoop.exe unregister`) -- Delete all the files that came with the program +1. Run `unregister.bat` as Administrator +2. Delete all the files that came with the program + +## Linux -## Set up for Linux
Flatpak - + - Download on Flathub + Get it on Flathub - Or run `flatpak install flathub com.steamgriddb.SGDBoop` +Or run `flatpak install flathub com.steamgriddb.SGDBoop` +
Build from source - - 1. Install the following prerequisites via your distros package manager: `make` `gcc` `libcurl4-openssl-dev` - 2. ```sh - git clone https://github.com/SteamGridDB/SGDBoop.git - sudo make install -C SGDBoop - ``` + +1. Install the following prerequisites via your distros package manager: `make` `gcc` `libcurl4-openssl-dev` `libgtk-3-dev` +2. + ```Shell + git clone https://github.com/SteamGridDB/SGDBoop.git + sudo make install -C SGDBoop + ``` +
Prebuilt binary via install script - - 1. Download the latest [sgdboop-linux64.tar.gz](https://github.com/SteamGridDB/SGDBoop/releases/latest) - 2. ```sh - mkdir sgdboop-linux64 - tar -zxf sgdboop-linux64.tar.gz -C sgdboop-linux64 - chmod +x sgdboop-linux64/install.sh - sudo ./sgdboop-linux64/install.sh - ``` + +1. Download the latest [.tar.gz](https://github.com/SteamGridDB/SGDBoop/releases/latest) matching your architecture +2. + ```Shell + mkdir sgdboop + tar -zxf sgdboop-linux-*.tar.gz -C sgdboop + chmod +x sgdboop/install.sh + sudo ./sgdboop/install.sh + ``` +
AUR for Arch Linux - - Install [the AUR package](https://aur.archlinux.org/packages/sgdboop). This can be done with an AUR manager like `yay`: - `yay -S sgdboop` + +Install the AUR package [sgdboop](https://aur.archlinux.org/packages/sgdboop) or [sgdboop-bin](https://aur.archlinux.org/packages/sgdboop-bin). This can be done with an AUR manager like `yay`: +`yay -S sgdboop` +These packages are maintained and updated by the community. +
-### Once installed, head over to https://www.steamgriddb.com/boop to enable the "**BOOP**" buttons! +## MacOS + +1. Download the latest [sgdboop-darwin-universal.zip](https://github.com/SteamGridDB/SGDBoop/releases/latest/download/sgdboop-darwin-universal.zip) +2. Extract `SGDBoop.app` to the `/Applications` directory +3. Open Terminal and run: `xattr -dr com.apple.quarantine /Applications/SGDBoop.app` +4. Run SGDBoop from Apps at least once + +### Once installed, head over to to enable the "**BOOP**" buttons! + +------ + +## Debugging + +To enable debugging messages in the log file, set the `SGDBOOP_DEBUG` environment variable to 1, or create a `SGDBOOP_DEBUG` (without any extension) file next to the executable. # Credits + # License + [zlib](LICENSE) diff --git a/SGDBoop-VS.vcxproj b/SGDBoop-VS.vcxproj index 328878c..37f5ea7 100644 --- a/SGDBoop-VS.vcxproj +++ b/SGDBoop-VS.vcxproj @@ -113,16 +113,23 @@ false - $(LibraryPath) + $(LibraryPath) false - $(LibraryPath) + $(LibraryPath) false true + + + copy /Y "$(ProjectDir)res\unregister.bat" "$(OutDir)" + copy /Y "$(ProjectDir)res\toggle_debug.bat" "$(OutDir)" + copy /Y "$(ProjectDir)LICENSE" "$(OutDir)LICENSE.txt" + + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) @@ -167,10 +174,6 @@ comctl32.lib;%(AdditionalDependencies) Windows - - - - false @@ -191,10 +194,6 @@ comctl32.lib;%(AdditionalDependencies) Windows - - - - false @@ -204,10 +203,6 @@ - - - - comctl32.lib;%(AdditionalDependencies) Windows @@ -225,10 +220,6 @@ - - - - comctl32.lib;%(AdditionalDependencies) Windows diff --git a/com.steamgriddb.SGDBoop.appdata.xml b/com.steamgriddb.SGDBoop.appdata.xml index b849e6c..5de427c 100644 --- a/com.steamgriddb.SGDBoop.appdata.xml +++ b/com.steamgriddb.SGDBoop.appdata.xml @@ -11,6 +11,46 @@ Zlib + + +
    +
  • Fixed some Steam apps not showing in Steam tab.
  • +
  • Fixed Steam app titles showing app name instead of Store name. Now properly matches the name shown in the client.
  • +
  • Fixed memory leak when showing API error.
  • +
+
+
+ + +
    +
  • Fixed an overflow bug in the test popup that was causing it not to show up
  • +
  • Added basic debugging logging
  • +
  • Renamed log file to sgdboop.log
  • +
+
+
+ + +
    +
  • Fixed crashes related to invalid mod paths for Linux releases
  • +
+
+
+ + +
    +
  • SGDBoop is now built and distributed for the ARM architecture
  • +
  • Added ability to apply assets to other Steam games via the (+) button.
  • +
  • Added ability to override the Steam base directory via the `BOOP_STEAMPATH` env var.
  • +
  • Updated the test popup to show some debug info, so users can potentially spot issues with their setup.
  • +
  • Fixed non-Steam games not being found on the latest version of Steam.
  • +
  • Fixed images in the Steam client sometimes looking corrupt/blank until a restart when on a slow network connection.
  • +
  • Fixed issue where some official Steam assets wont apply due to not following URL redirects.
  • +
  • Fixed some Source mods not being detected.
  • +
  • Fixed tons of crashes, memory leaks, edge cases.
  • +
+
+
    diff --git a/gui-helper-gtk.c b/gui-helper-gtk.c index a8d7b1a..d3050a8 100644 --- a/gui-helper-gtk.c +++ b/gui-helper-gtk.c @@ -40,6 +40,7 @@ typedef struct { typedef struct { TabData* tab1; TabData* tab2; + TabData* tab3; } TabsContext; static void button_callback(gpointer data) @@ -66,10 +67,14 @@ static void on_tab_switch(GtkNotebook* notebook, GtkWidget* page, guint page_num { g_object_set_data(G_OBJECT(btn), "tabdata", ctx->tab1); } - else + else if (page_num == 1) { g_object_set_data(G_OBJECT(btn), "tabdata", ctx->tab2); } + else + { + g_object_set_data(G_OBJECT(btn), "tabdata", ctx->tab3); + } } static void on_ok_button_clicked(GtkButton* button, gpointer user_data) @@ -81,7 +86,7 @@ static void on_ok_button_clicked(GtkButton* button, gpointer user_data) GtkWidget* create_treeview(const char** items, int count, int initial_selection, int* selected_index) { GtkListStore* store = gtk_list_store_new( - 2, // 2 tabs + 2, // 2 columns G_TYPE_STRING, G_TYPE_INT ); @@ -119,7 +124,7 @@ GtkWidget* create_treeview(const char** items, int count, int initial_selection, return treeview; } -int SelectionDialog(const char* title, int count, const char** list, int modsCount, const char** modsList, int selection) +int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamList, int modsCount, const char** modsList, int steamCount, const char** steamList, int selection) { gtk_init(NULL, NULL); int selected_index = -1; @@ -147,15 +152,19 @@ int SelectionDialog(const char* title, int count, const char** list, int modsCou gtk_box_pack_start(GTK_BOX(box), notebook, TRUE, TRUE, 0); int selectionTab = 0; - if (selection >= count) { + if (selection >= nonSteamCount + modsCount) { + selectionTab = 2; + selection -= nonSteamCount + modsCount; + } + if (selection >= nonSteamCount) { selectionTab = 1; - selection -= count; + selection -= nonSteamCount; } // Non-Steam apps tab TabData* nonsteam_data = g_new0(TabData, 1); nonsteam_data->tab_index = 0; - nonsteam_data->treeview = create_treeview(list, count, selection, &nonsteam_data->selected_index); + nonsteam_data->treeview = create_treeview(nonSteamList, nonSteamCount, selection, &nonsteam_data->selected_index); GtkWidget* scroll1 = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(scroll1), nonsteam_data->treeview); @@ -170,10 +179,19 @@ int SelectionDialog(const char* title, int count, const char** list, int modsCou gtk_container_add(GTK_CONTAINER(scroll2), mods_data->treeview); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scroll2, gtk_label_new("GoldSrc/Source Mods")); + // Steam tab + TabData* steam_data = g_new0(TabData, 1); + steam_data->tab_index = 2; + steam_data->treeview = create_treeview(steamList, steamCount, selection, &steam_data->selected_index); + + GtkWidget* scroll3 = gtk_scrolled_window_new(NULL, NULL); + gtk_container_add(GTK_CONTAINER(scroll3), steam_data->treeview); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scroll3, gtk_label_new("Steam")); + TabsContext* context = g_malloc(sizeof(TabsContext)); context->tab1 = nonsteam_data; context->tab2 = mods_data; - //context->tab3 = steam_data; + context->tab3 = steam_data; // Button contianer GtkWidget* buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); @@ -206,10 +224,14 @@ int SelectionDialog(const char* title, int count, const char** list, int modsCou gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), 0); g_object_set_data(G_OBJECT(ok_button), "tabdata", context->tab1); } - else { + else if (selectionTab == 1) { gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), 1); g_object_set_data(G_OBJECT(ok_button), "tabdata", context->tab2); } + else if (selectionTab == 2) { + gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), 2); + g_object_set_data(G_OBJECT(ok_button), "tabdata", context->tab3); + } gtk_main(); @@ -218,9 +240,10 @@ int SelectionDialog(const char* title, int count, const char** list, int modsCou int final_selection = final_tabdata->selected_index; g_free(nonsteam_data); g_free(mods_data); + g_free(steam_data); if (final_tabdata->tab_index > 0) { - final_selection += count; + final_selection += nonSteamCount; } return final_selection; } diff --git a/gui-helper-mac.m b/gui-helper-mac.m new file mode 100644 index 0000000..7e3dbfe --- /dev/null +++ b/gui-helper-mac.m @@ -0,0 +1,457 @@ +// Cocoa implementation in Objective-C, C-only gimmick not worth it for Apple +// https://developer.apple.com/documentation/appkit?language=objc + +#include "gui-helper.h" +#include +#include +#include +#include + +static NSString *NSStringFromCString(const char *str) +{ + return [NSString stringWithUTF8String:(str != NULL) ? str : ""]; +} + +// https://developer.apple.com/documentation/appkit/nsalert?language=objc +int ShowMessageBox(const char *title, const char *message) +{ + [NSApplication sharedApplication]; + [NSApp activateIgnoringOtherApps:YES]; + + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:NSStringFromCString(title)]; + [alert setInformativeText:NSStringFromCString(message)]; + [alert addButtonWithTitle:@"OK"]; + + NSInteger response = [alert runModal]; + + return (int)response; +} + +@interface SGDBSelectionController : NSObject +{ +@public + const char** nonSteamItems; + int nonSteamCount; + const char** modsItems; + int modsCount; + const char** steamItems; + int steamCount; + int result; + BOOL done; +} + +@property (nonatomic, strong) NSWindow* window; +@property (nonatomic, weak) NSSegmentedControl* segmentedControl; +@property (nonatomic, weak) NSScrollView* nonSteamScrollView; +@property (nonatomic, weak) NSScrollView* modsScrollView; +@property (nonatomic, weak) NSScrollView* steamScrollView; +@property (nonatomic, weak) NSTableView* nonSteamTable; +@property (nonatomic, weak) NSTableView* modsTable; +@property (nonatomic, weak) NSTableView* steamTable; +@property (nonatomic, weak) NSButton* okButton; + +- (id)initWithTitle:(const char*)title + nonSteamItems:(const char**)nsItems + nonSteamCount:(int)nsCount + modsItems:(const char**)mItems + modsCount:(int)mCount + steamItems:(const char**)sItems + steamCount:(int)sCount + initialTab:(int)initialTab + initialSelection:(int)initialSelection; + +- (NSScrollView*)buildScrollingTableWithTag:(NSInteger)tag outTableView:(NSTableView**)outTableView; +- (void)segmentChanged:(id)sender; +- (void)okClicked:(id)sender; +- (void)cancelClicked:(id)sender; + +@end + +@implementation SGDBSelectionController + +- (id)initWithTitle:(const char*)title + nonSteamItems:(const char**)nsItems + nonSteamCount:(int)nsCount + modsItems:(const char**)mItems + modsCount:(int)mCount + steamItems:(const char**)sItems + steamCount:(int)sCount + initialTab:(int)initialTab + initialSelection:(int)initialSelection +{ + self = [super init]; + if (!self) { + return nil; + } + + nonSteamItems = nsItems; + nonSteamCount = nsCount; + modsItems = mItems; + modsCount = mCount; + steamItems = sItems; + steamCount = sCount; + result = -1; + done = NO; + + const NSRect contentRect = NSMakeRect(0, 0, 454, 388); + const NSUInteger styleMask = NSWindowStyleMaskTitled + | NSWindowStyleMaskClosable + | NSWindowStyleMaskMiniaturizable + | NSWindowStyleMaskResizable; + + NSWindow* window = [[NSWindow alloc] initWithContentRect:contentRect + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:NO]; + [window setTitle:NSStringFromCString(title)]; + [window setDelegate:self]; + [window center]; + self.window = window; + + NSView* contentView = [window contentView]; + + // Segmented control switches between three lists + NSSegmentedControl* segmentedControl = [ + NSSegmentedControl segmentedControlWithLabels:@[@"Non-Steam", @"GoldSrc/Source Mods", @"Steam"] + trackingMode:NSSegmentSwitchTrackingSelectOne + target:self + action:@selector(segmentChanged:) + ]; + [segmentedControl setFrame:NSMakeRect(10, 354, 434, 24)]; + [segmentedControl setSegmentStyle:NSSegmentStyleAutomatic]; + [segmentedControl setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)]; + self.segmentedControl = segmentedControl; + [contentView addSubview:segmentedControl]; + + NSTableView* nonSteamTable = nil; + NSScrollView* nonSteamScroll = [self buildScrollingTableWithTag:0 outTableView:&nonSteamTable]; + [nonSteamScroll setFrame:NSMakeRect(10, 50, 434, 294)]; + [nonSteamScroll setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; + self.nonSteamTable = nonSteamTable; + self.nonSteamScrollView = nonSteamScroll; + [contentView addSubview:nonSteamScroll]; + + NSTableView* modsTable = nil; + NSScrollView* modsScroll = [self buildScrollingTableWithTag:1 outTableView:&modsTable]; + [modsScroll setFrame:NSMakeRect(10, 50, 434, 294)]; + [modsScroll setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; + self.modsTable = modsTable; + self.modsScrollView = modsScroll; + [contentView addSubview:modsScroll]; + + NSTableView* steamTable = nil; + NSScrollView* steamScroll = [self buildScrollingTableWithTag:2 outTableView:&steamTable]; + [steamScroll setFrame:NSMakeRect(10, 50, 434, 294)]; + [steamScroll setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; + self.steamTable = steamTable; + self.steamScrollView = steamScroll; + [contentView addSubview:steamScroll]; + + // buttons, on mac the standard is [Cancel] [OK] + NSButton* okButton = [[NSButton alloc] initWithFrame:NSMakeRect(454 - 80 - 10, 10, 80, 30)]; + [okButton setTitle:@"OK"]; + [okButton setBezelStyle:NSBezelStyleAutomatic]; + [okButton setAutoresizingMask:(NSViewMinXMargin | NSViewMaxYMargin)]; + [okButton setTarget:self]; + [okButton setAction:@selector(okClicked:)]; + [okButton setKeyEquivalent:@"\r"]; + [contentView addSubview:okButton]; + self.okButton = okButton; + + NSButton* cancelButton = [[NSButton alloc] initWithFrame:NSMakeRect(454 - 80 - 10 - 80 - 10, 10, 80, 30)]; + [cancelButton setTitle:@"Cancel"]; + [cancelButton setBezelStyle:NSBezelStyleAutomatic]; + [cancelButton setAutoresizingMask:(NSViewMinXMargin | NSViewMaxYMargin)]; + [cancelButton setTarget:self]; + [cancelButton setAction:@selector(cancelClicked:)]; + [cancelButton setKeyEquivalent:@"\e"]; // Escape key + [contentView addSubview:cancelButton]; + + // apply initial tab + selection + if (initialTab < 0 || initialTab > 2) { + initialTab = 0; + } + [segmentedControl setSelectedSegment:initialTab]; + [nonSteamScroll setHidden:(initialTab != 0)]; + [modsScroll setHidden:(initialTab != 1)]; + [steamScroll setHidden:(initialTab != 2)]; + + NSTableView* initialTable = nonSteamTable; + int initialCount = nonSteamCount; + if (initialTab == 1) { + initialTable = modsTable; + initialCount = modsCount; + } else if (initialTab == 2) { + initialTable = steamTable; + initialCount = steamCount; + } + if (initialSelection >= 0 && initialSelection < initialCount) { + [initialTable selectRowIndexes:[NSIndexSet indexSetWithIndex:initialSelection] byExtendingSelection:NO]; + [initialTable scrollRowToVisible:initialSelection]; + [self.okButton setEnabled:YES]; + } else { + [self.okButton setEnabled:NO]; + } + + return self; +} + +// Builds a headerless, single-column table view inside a scroll view. +- (NSScrollView*)buildScrollingTableWithTag:(NSInteger)tag outTableView:(NSTableView**)outTableView +{ + NSTableColumn* column = [[NSTableColumn alloc] initWithIdentifier:@"item"]; + [column setWidth:400]; + [column setEditable:NO]; + + NSTableView* tableView = [[NSTableView alloc] initWithFrame:NSMakeRect(0, 0, 434, 328)]; + [tableView addTableColumn:column]; + [tableView setHeaderView:nil]; + [tableView setAllowsMultipleSelection:NO]; + [tableView setAllowsEmptySelection:YES]; + [tableView setTag:tag]; + [tableView setDataSource:self]; + [tableView setDelegate:self]; + [tableView setTarget:self]; + [tableView setDoubleAction:@selector(okClicked:)]; + + NSScrollView* scrollView = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, 434, 328)]; + [scrollView setDocumentView:tableView]; + [scrollView setHasVerticalScroller:YES]; + [scrollView setAutohidesScrollers:YES]; + [scrollView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; + + if (outTableView != NULL) { + *outTableView = tableView; + } + return scrollView; +} + +- (NSTableView*)tableViewForSegment:(NSInteger)segment +{ + switch (segment) { + case 0: return self.nonSteamTable; + case 1: return self.modsTable; + case 2: return self.steamTable; + default: return nil; + } +} + +- (void)segmentChanged:(id)sender +{ + NSInteger selected = [self.segmentedControl selectedSegment]; + [self.nonSteamScrollView setHidden:(selected != 0)]; + [self.modsScrollView setHidden:(selected != 1)]; + [self.steamScrollView setHidden:(selected != 2)]; + + NSTableView* activeTable = [self tableViewForSegment:selected]; + [self.okButton setEnabled:([activeTable selectedRow] != -1)]; +} + +- (void)okClicked:(id)sender +{ + NSInteger tabIndex = [self.segmentedControl selectedSegment]; + NSTableView* activeTable = [self tableViewForSegment:tabIndex]; + + NSInteger row = [activeTable selectedRow]; + if (row < 0) { + return; // nothing selected; OK should be disabled in this case anyway + } + + result = (int)row; + if (tabIndex == 1) { + result += nonSteamCount; + } else if (tabIndex == 2) { + result += nonSteamCount + modsCount; + } + + done = YES; + [NSApp stopModal]; +} + +- (void)cancelClicked:(id)sender +{ + result = -1; + done = YES; + [NSApp stopModal]; +} + +// NSTableViewDataSource + +- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView +{ + switch ([tableView tag]) { + case 0: return nonSteamCount; + case 1: return modsCount; + case 2: return steamCount; + default: return 0; + } +} + +// Custom vertically centered item + 4 padding + +- (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row +{ + static NSString* const identifier = @"SGDBItemCell"; + + NSTableCellView* cellView = [tableView makeViewWithIdentifier:identifier owner:self]; + if (cellView == nil) { + cellView = [[NSTableCellView alloc] initWithFrame:NSMakeRect(0, 0, tableColumn.width, tableView.rowHeight)]; + cellView.identifier = identifier; + + NSTextField* textField = [NSTextField labelWithString:@""]; + textField.translatesAutoresizingMaskIntoConstraints = NO; + textField.lineBreakMode = NSLineBreakByTruncatingTail; + [cellView addSubview:textField]; + cellView.textField = textField; + + [NSLayoutConstraint activateConstraints:@[ + [textField.leadingAnchor constraintEqualToAnchor:cellView.leadingAnchor constant:4], + [textField.trailingAnchor constraintEqualToAnchor:cellView.trailingAnchor constant:-4], + [textField.centerYAnchor constraintEqualToAnchor:cellView.centerYAnchor], + ]]; + } + + const char* str = NULL; + switch ([tableView tag]) { + case 0: str = nonSteamItems[row]; break; + case 1: str = modsItems[row]; break; + case 2: str = steamItems[row]; break; + default: break; + } + cellView.textField.stringValue = NSStringFromCString(str); + + return cellView; +} + +// NSTableViewDelegate + +- (void)tableViewSelectionDidChange:(NSNotification*)notification +{ + NSTableView* tableView = [notification object]; + [self.okButton setEnabled:([tableView selectedRow] != -1)]; +} + +// NSWindowDelegate - clicking the red close button counts as Cancel. + +- (void)windowWillClose:(NSNotification*)notification +{ + if (!done) { + result = -1; + done = YES; + } + if ([NSApp modalWindow] == self.window) { + [NSApp stopModal]; + } +} + +@end + +int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamList, int modsCount, const char** modsList, int steamCount, const char** steamList, int selection) +{ + int returnValue = -1; + + @autoreleasepool { + [NSApplication sharedApplication]; + + int initialTab = 0; + int initialSelection = selection; + if (selection >= nonSteamCount + modsCount) { + initialTab = 2; + initialSelection -= nonSteamCount + modsCount; + } else if (selection >= nonSteamCount) { + initialTab = 1; + initialSelection -= nonSteamCount; + } + + SGDBSelectionController* controller = [[SGDBSelectionController alloc] + initWithTitle:title + nonSteamItems:nonSteamList + nonSteamCount:nonSteamCount + modsItems:modsList + modsCount:modsCount + steamItems:steamList + steamCount:steamCount + initialTab:initialTab + initialSelection:initialSelection]; + + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp activateIgnoringOtherApps:YES]; + [controller.window makeKeyAndOrderFront:nil]; + [NSApp runModalForWindow:controller.window]; + + returnValue = controller->result; + + [controller.window orderOut:nil]; + } + + return returnValue; +} + +static void stopRunLoop(void) +{ + [NSApp stop:nil]; + NSEvent* dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:nil subtype:0 data1:0 data2:0]; + [NSApp postEvent:dummy atStart:YES]; +} + +@interface SGDBAppDelegate : NSObject +@property (nonatomic, copy) NSString* URL; +@end + +@implementation SGDBAppDelegate + +- (void)application:(NSApplication *)application openURLs:(NSArray *)urls +{ + if (urls.count > 0) { + self.URL = [urls[0] absoluteString]; + stopRunLoop(); + } +} + +@end + +const char* macAwaitEvent() +{ + const double timeout = 1.5; + NSApplication* app = [NSApplication sharedApplication]; + + SGDBAppDelegate* delegate = [[SGDBAppDelegate alloc] init]; + app.delegate = delegate; + + NSTimer* timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:timeout repeats:NO block:^(NSTimer* t) { + (void)t; + stopRunLoop(); + }]; + + [app run]; + + [timeoutTimer invalidate]; + app.delegate = nil; + + static char url[512]; + url[0] = '\0'; + if (delegate.URL) { + strncpy(url, [delegate.URL UTF8String], sizeof(url) - 1); + url[sizeof(url) - 1] = '\0'; + return url; + } + + return NULL; +} + +// Set the URL by using the old depricated API cause the new method is annoying to implement +// https://developer.apple.com/documentation/coreservices/1447760-lssetdefaulthandlerforurlscheme?language=objc +int macSetURLHandler() +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + OSStatus status = LSSetDefaultHandlerForURLScheme(CFSTR("sgdb"), CFSTR("com.steamgriddb.SGDBoop")); + if (status == noErr) { + ShowMessageBox("SGDBoop Information", "Program registered successfully!\n\nSGDBoop is meant to be ran from a browser!\nHead over to https://www.steamgriddb.com/boop to continue setup."); + } else { + ShowMessageBox("SGDBoop Information", "Error registering URL scheme."); + } + return 0; +#pragma clang diagnostic pop +} \ No newline at end of file diff --git a/gui-helper-win.c b/gui-helper-win.c index 178c135..1214385 100644 --- a/gui-helper-win.c +++ b/gui-helper-win.c @@ -14,8 +14,8 @@ static HWND hWndButtonOk; static HWND hWndButtonCancel; static int selected_index = -1; static HWND hWndTab; -static const char** tabLists[2]; -static int tabCounts[2]; +static const char** tabLists[3]; +static int tabCounts[3]; static LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) @@ -45,8 +45,8 @@ static LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lPara tie.pszText = "GoldSrc/Source Mods"; TabCtrl_InsertItem(hWndTab, 1, &tie); - //tie.pszText = "Steam Apps"; - //TabCtrl_InsertItem(hWndTab, 2, &tie); + tie.pszText = "Steam"; + TabCtrl_InsertItem(hWndTab, 2, &tie); // ListBox hWndList = CreateWindowExA( @@ -167,18 +167,20 @@ int ShowMessageBoxW(const wchar_t* title, const wchar_t* message) return MessageBoxW(NULL, message, title, MB_OK); } -int SelectionDialog(const char* title, int count, const char** list, int modsCount, const char** modsList, int selection) +int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamList, int modsCount, const char** modsList, int steamCount, const char** steamList, int selection) { INITCOMMONCONTROLSEX icex; icex.dwSize = sizeof(INITCOMMONCONTROLSEX); icex.dwICC = ICC_TAB_CLASSES; InitCommonControlsEx(&icex); - tabCounts[0] = count; + tabCounts[0] = nonSteamCount; tabCounts[1] = modsCount; + tabCounts[2] = steamCount; - tabLists[0] = list; + tabLists[0] = nonSteamList; tabLists[1] = modsList; + tabLists[2] = steamList; MSG Msg; HWND hWnd; diff --git a/gui-helper.h b/gui-helper.h index 8cbd7fa..3340ae3 100644 --- a/gui-helper.h +++ b/gui-helper.h @@ -6,10 +6,15 @@ int ShowMessageBox(const char* title, const char* message); void PopulateListBoxWithSelection(int tabIndex, int selection); void PopulateListBox(int tabIndex); -int SelectionDialog(const char * title, int count, const char** list, int modsCount, const char** modsList, int selection); - +int SelectionDialog(const char* title, int nonSteamCount, const char** nonSteamList, int modsCount, const char** modsList, int steamCount, const char** steamList, int selection); #if defined(_WIN32) || defined(WIN32) int ShowMessageBoxW(const wchar_t* title, const wchar_t* message); // This function is only used in Windows. It shouldn't exist, but we have to deal with Microsoft. wchar_t * ConvertStringToUnicode(const char* string); +#endif + +// expose some Objective-C things to be used cause i cant be arsed +#ifdef __APPLE__ +const char* macAwaitEvent(); +int macSetURLHandler(); #endif \ No newline at end of file diff --git a/linux-release/com.steamgriddb.SGDBoop-flatpak.desktop b/res/linux/com.steamgriddb.SGDBoop-flatpak.desktop similarity index 100% rename from linux-release/com.steamgriddb.SGDBoop-flatpak.desktop rename to res/linux/com.steamgriddb.SGDBoop-flatpak.desktop diff --git a/linux-release/com.steamgriddb.SGDBoop.desktop b/res/linux/com.steamgriddb.SGDBoop.desktop similarity index 100% rename from linux-release/com.steamgriddb.SGDBoop.desktop rename to res/linux/com.steamgriddb.SGDBoop.desktop diff --git a/linux-release/install.sh b/res/linux/install.sh similarity index 100% rename from linux-release/install.sh rename to res/linux/install.sh diff --git a/res/mac/AppIcon.icns b/res/mac/AppIcon.icns new file mode 100644 index 0000000..9a912cf Binary files /dev/null and b/res/mac/AppIcon.icns differ diff --git a/res/mac/Apple Icon.icon/Assets/layer1.svg b/res/mac/Apple Icon.icon/Assets/layer1.svg new file mode 100644 index 0000000..773c69d --- /dev/null +++ b/res/mac/Apple Icon.icon/Assets/layer1.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/mac/Apple Icon.icon/Assets/layer2.svg b/res/mac/Apple Icon.icon/Assets/layer2.svg new file mode 100644 index 0000000..e4ec77c --- /dev/null +++ b/res/mac/Apple Icon.icon/Assets/layer2.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/mac/Apple Icon.icon/Assets/layer3.svg b/res/mac/Apple Icon.icon/Assets/layer3.svg new file mode 100644 index 0000000..d4ce829 --- /dev/null +++ b/res/mac/Apple Icon.icon/Assets/layer3.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/mac/Apple Icon.icon/icon.json b/res/mac/Apple Icon.icon/icon.json new file mode 100644 index 0000000..b77a563 --- /dev/null +++ b/res/mac/Apple Icon.icon/icon.json @@ -0,0 +1,92 @@ +{ + "fill-specializations" : [ + { + "value" : { + "linear-gradient" : [ + "display-p3:0.41284,0.65967,0.82471,1.00000", + "display-p3:0.25391,0.44434,0.60059,1.00000" + ], + "orientation" : { + "start" : { + "x" : 0.5, + "y" : 0 + }, + "stop" : { + "x" : 0.5, + "y" : 0.7 + } + } + } + }, + { + "appearance" : "dark", + "value" : "system-dark" + } + ], + "groups" : [ + { + "blur-material" : 0.5, + "layers" : [ + { + "fill" : { + "linear-gradient" : [ + "display-p3:0.47046,0.74414,0.91895,1.00000", + "display-p3:0.22900,0.35278,0.46411,1.00000" + ], + "orientation" : { + "start" : { + "x" : 1, + "y" : 0 + }, + "stop" : { + "x" : 0.16296886421783632, + "y" : 0.8473250182748537 + } + } + }, + "image-name" : "layer1.svg", + "name" : "layer1" + }, + { + "fill" : { + "solid" : "display-p3:0.45557,0.69824,0.92041,1.00000" + }, + "glass" : true, + "hidden" : false, + "image-name" : "layer2.svg", + "name" : "layer2" + }, + { + "fill" : { + "solid" : "display-p3:0.22900,0.35278,0.46411,1.00000" + }, + "image-name" : "layer3.svg", + "name" : "layer3" + } + ], + "lighting" : "individual", + "position" : { + "scale" : 1.8, + "translation-in-points" : [ + -20, + 20 + ] + }, + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "specular" : true, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/res/mac/Assets.car b/res/mac/Assets.car new file mode 100644 index 0000000..67dc24a Binary files /dev/null and b/res/mac/Assets.car differ diff --git a/res/mac/Info.plist b/res/mac/Info.plist new file mode 100644 index 0000000..81efdd2 --- /dev/null +++ b/res/mac/Info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleSupportedPlatforms + + MacOSX + + CFBundleName + SGDBoop + CFBundleIdentifier + com.steamgriddb.SGDBoop + CFBundleExecutable + SGDBoop + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.3.2 + CFBundleVersion + 1.3.2 + CFBundleInfoDictionaryVersion + 6.0 + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + + LSUIElement + + LSMinimumSystemVersion + 10.15.0 + CFBundleURLTypes + + + CFBundleURLName + com.steamgriddb.SGDBoop + CFBundleURLIconFile + AppIcon.icns + CFBundleURLSchemes + + sgdb + + + + NSHighResolutionCapable + + NSHumanReadableCopyright + SteamGridDB, LLC + + diff --git a/res/mac/README.md b/res/mac/README.md new file mode 100644 index 0000000..4290ed6 --- /dev/null +++ b/res/mac/README.md @@ -0,0 +1 @@ +`AppIcon.icns` & `Assets.car` is generated from `Apple Icon.icon` by making a dummy Xcode project, importing `Apple Icon.icon`, then checking "Include All App Icon Assets" under "General -> App Icons and Launch Screen" and "Build Settings -> Asset Catalog Compiler" the icon name should also be set under those options to whatever the file name is. Finally just build the app and grab the 2 files from the built Hello World .app. \ No newline at end of file diff --git a/res/toggle_debug.bat b/res/toggle_debug.bat new file mode 100644 index 0000000..d3702c8 --- /dev/null +++ b/res/toggle_debug.bat @@ -0,0 +1,20 @@ +@echo off +cd /d "%~dp0" + +:: Check if the bat is being run as admin +net session >nul 2>&1 +if not %errorlevel% == 0 ( + echo Please run this script as Administrator. + pause + exit +) + +if "%SGDBOOP_DEBUG%" == "1" ( + setx SGDBOOP_DEBUG 0 /m >nul + echo Disabled debugging. Run this script again to enable it. +) else ( + setx SGDBOOP_DEBUG 1 /m >nul + echo Enabled debugging. Run this script again to disable it. +) + +pause \ No newline at end of file diff --git a/res/unregister.bat b/res/unregister.bat new file mode 100644 index 0000000..087acf1 --- /dev/null +++ b/res/unregister.bat @@ -0,0 +1,12 @@ +@echo off +cd /d "%~dp0" + +:: Check if the bat is being run as admin +net session >nul 2>&1 +if not %errorlevel% == 0 ( + echo Please run this script as Administrator. + pause + exit +) + +SGDBoop.exe unregister \ No newline at end of file diff --git a/sgdboop.c b/sgdboop.c index b54cfc3..628d61b 100644 --- a/sgdboop.c +++ b/sgdboop.c @@ -12,21 +12,45 @@ #include "gui-helper.h" #include "crc.h" +#define VERSION "1.4.3" // this is also parsed by the makefile for dist +#define API_VERSION "3" +#define API_USER_AGENT "SGDBoop/v" VERSION -#ifdef __unix__ /* __unix__ is usually defined by compilers targeting Unix systems */ -#define OS_Windows 0 -#define MAX_PATH 600 +typedef struct AppStruct +{ + int index; + char name[300]; + char appid[128]; + char appid_old[128]; + char type[50]; +} AppStruct; + +#ifdef __APPLE__ +#define OS_Mac 1 +#endif +#ifdef __linux__ +#define OS_Linux 1 +#endif + +// Shims for Unix systems +#if defined(OS_Mac) || defined(OS_Linux) #define FALSE 0 #define TRUE 1 + +#ifndef MAX_PATH +#ifdef PATH_MAX +#define MAX_PATH PATH_MAX +#else +#define MAX_PATH 1024 +#endif +#endif + #define WCHAR char #define LPSTR char* -int GetConsoleWindow(); -void MoveWindow(int, int, int, int, int, int); -void GetModuleFileName(char*, char*, int); -FILE* _popen(char*, char*); -void _pclose(FILE*); #include -#elif defined(_WIN32) || defined(WIN32) /* _Win32 is usually defined by compilers targeting 32 or 64 bit Windows systems */ + +// Windows Only APIs +#elif defined(_WIN32) || defined(WIN32) #define OS_Windows 1 #include int symlink(char* a, char* b) { @@ -85,23 +109,13 @@ int setWindowsRegistryString(HKEY key, char* subKey, char* valueName, char* valu } #endif -#define API_VERSION "3" -#define API_USER_AGENT "SGDBoop/v1.3.1" - -typedef struct nonSteamApp -{ - int index; - char name[300]; - char appid[128]; - char appid_old[128]; - char type[50]; -} nonSteamApp; - -int _nonSteamAppsCount = 0; -int _modsCount = 0; -int _sourceModsCount = 0; -int _goldSourceModsCount = 0; -int _apiReturnedLines = 0; +unsigned int _nonSteamAppsCount = 0; +unsigned int _steamAppsCount = 0; +unsigned int _modsCount = 0; +unsigned int _sourceModsCount = 0; +unsigned int _goldSourceModsCount = 0; +unsigned int _apiReturnedLines = 0; +unsigned int DEBUG = 0; // Get logfile path char* getLogFilepath() { @@ -116,8 +130,8 @@ char* getLogFilepath() { } *filename = '\0'; strcpy(logFilepath, (const char*)path); - strcat(logFilepath, "sgdboop_error.log"); -#else + strcat(logFilepath, "sgdboop.log"); +#elif OS_Linux if (getenv("XDG_STATE_HOME") != NULL && strlen(getenv("XDG_STATE_HOME")) > 0) { strcpy(logFilepath, getenv("XDG_STATE_HOME")); } @@ -131,14 +145,17 @@ char* getLogFilepath() { mkdir(logFilepath, 0700); } - strcat(logFilepath, "/sgdboop_error.log"); + strcat(logFilepath, "/sgdboop.log"); +#elif OS_Mac + strcpy(logFilepath, getenv("HOME")); + strcat(logFilepath, "/Library/Logs/sgdboop.log"); #endif return logFilepath; } -// Log error messages -void logError(const char* error, const int errorCode) +// Log messages +void logMessage(const char* message, const int identifier) { time_t now = time(0); time_t rawtime; @@ -148,20 +165,27 @@ void logError(const char* error, const int errorCode) char* logFilepath = getLogFilepath(); + char messageType[10]; + if (identifier == 0) { + strcpy(messageType, "[INFO]"); + } else { + strcpy(messageType, "[ERROR]"); + } + FILE* logFile = fopen(logFilepath, "a"); if (logFile == NULL) { logFile = fopen(logFilepath, "w"); } if (logFile) { - fprintf(logFile, "%s%s [%d]\n\n", asctime(timeinfo), error, errorCode); + fprintf(logFile, "%s%s %s [%d]\n\n", asctime(timeinfo), messageType, message, identifier); fclose(logFile); - printf("Created logfile in %s\n", logFilepath); + printf("Logged to %s\n", logFilepath); } } // Log an error and exit with the given error code -void exitWithError(const char* error, const int errorCode) { - logError(error, errorCode); +void exitWithError(const char* errorMessage, const int errorCode) { + logMessage(errorMessage, errorCode); exit(errorCode); } @@ -181,9 +205,7 @@ void cleanupOldAssetFiles(const char* finalPath) strcpy(tempPath, basePath); strcat(tempPath, extensions[i]); - if (strcmp(tempPath, finalPath) != 0) { - remove(tempPath); - } + remove(tempPath); } } @@ -220,6 +242,7 @@ char*** callAPI(char* grid_types, char* grid_ids, char* mode) curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); curl_easy_setopt(curl, CURLOPT_FAILONERROR, FALSE); curl_easy_setopt(curl, CURLOPT_USERAGENT, API_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE); headers = curl_slist_append(headers, authHeader); headers = curl_slist_append(headers, apiVersionHeader); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); @@ -235,7 +258,7 @@ char*** callAPI(char* grid_types, char* grid_ids, char* mode) strcat(message, s.ptr); if (startsWith(s.ptr, "error-")) { - strreplace(message, "error-", " "); + message = strreplace(message, "error-", ""); ShowMessageBox("SGDBoop Error", message); } exitWithError(message, (int)http_code); @@ -276,7 +299,7 @@ char*** callAPI(char* grid_types, char* grid_ids, char* mode) } // Download an asset file -char* downloadAssetFile(char* app_id, char* url, char* type, char* orientation, char* asset_hash, char* destinationDir, struct nonSteamApp* nonSteamAppData) +char* downloadAssetFile(char* app_id, char* url, char* type, char* orientation, char* asset_hash, char* destinationDir, struct AppStruct* appData) { // Try creating folder if (access(destinationDir, 0) != 0) { @@ -287,7 +310,7 @@ char* downloadAssetFile(char* app_id, char* url, char* type, char* orientation, FILE* fp; CURLcode res; - char* outfilename = malloc(1024); + char* outfilename = malloc(MAX_PATH); strcpy(outfilename, destinationDir); strcat(outfilename, app_id); if (strcmp(type, "hero") == 0) { @@ -307,7 +330,7 @@ char* downloadAssetFile(char* app_id, char* url, char* type, char* orientation, } else if (strcmp(type, "icon") == 0) { - if (nonSteamAppData != NULL) { + if (appData != NULL) { // Add _icon to nonsteam icons strcat(outfilename, "_icon"); @@ -319,7 +342,7 @@ char* downloadAssetFile(char* app_id, char* url, char* type, char* orientation, } // Always save as jpg if icon and replacing default Steam - if (nonSteamAppData == NULL && strcmp(type, "icon") == 0) { + if (appData == NULL && strcmp(type, "icon") == 0) { strcat(outfilename, ".jpg"); } else { // Save as original file extension @@ -340,24 +363,37 @@ char* downloadAssetFile(char* app_id, char* url, char* type, char* orientation, } } + curl = curl_easy_init(); if (curl && outfilename != 0) { cleanupOldAssetFiles(outfilename); - fp = fopen(outfilename, "wb"); + + char* outfilename_temp = malloc(MAX_PATH); + strcpy(outfilename_temp, outfilename); + strcat(outfilename_temp, "_temp"); + + fp = fopen(outfilename_temp, "wb"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_FAILONERROR, TRUE); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_USERAGENT, API_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE); res = curl_easy_perform(curl); curl_easy_cleanup(curl); fclose(fp); if (res != 0) { - remove(outfilename); + remove(outfilename_temp); + free(outfilename_temp); free(outfilename); return NULL; } - return outfilename; + // Rename the file to trigger a refresh on the client + if (!rename(outfilename_temp, outfilename)) { + free(outfilename); + free(outfilename_temp); + return outfilename; + } } free(outfilename); @@ -383,11 +419,17 @@ int createURIprotocol() { char* ret_val_str = getWindowsRegistryString(HKEY_CLASSES_ROOT, "sgdb\\Shell\\Open\\Command", "default"); if (!ret_val_str) { ShowMessageBox("SGDBoop Error", "Please run this program as Administrator to register it!\n"); + free(regeditPath); + return 1; } else if (strcmp(ret_val_str, regeditPath) == 0) { ShowMessageBoxW(L"SGDBoop Information", L"SGDBoop is already registered!\nHead over to https://www.steamgriddb.com/boop to continue setup.\n\nIf you moved the program and want to register again, run SGDBoop as Administrator.\n"); + free(regeditPath); + return 0; + } else { + ShowMessageBoxW(L"SGDBoop Information", L"SGDBoop is already registered at another filepath. Run this SGDBoop executable as Administrator to register it.\n"); + free(regeditPath); + return 1; } - free(regeditPath); - return 1; } setWindowsRegistryString(HKEY_CLASSES_ROOT, "sgdb\\Shell\\Open\\Command", "default", regeditPath); @@ -402,16 +444,21 @@ int createURIprotocol() { strcpy(popupMessage, "Program registered successfully!\n\nSGDBoop is meant to be ran from a browser!\nHead over to https://www.steamgriddb.com/boop to continue setup."); strcat(popupMessage, "\n\nLog file path: "); strcat(popupMessage, logFilepath); + strcat(popupMessage, "\n\nTo enable or disable debugging messages, run toggle_debug.bat"); ShowMessageBoxW(L"SGDBoop Information", ConvertStringToUnicode(popupMessage)); free(regeditPath); return 0; -#else +#elif OS_Linux // Do nothing on linux strcpy(popupMessage, "SGDBoop is meant to be ran from a browser!\nHead over to https://www.steamgriddb.com/boop to continue setup."); strcat(popupMessage, "\n\nLog file path: "); strcat(popupMessage, logFilepath); ShowMessageBox("SGDBoop Information", popupMessage); return 0; +#elif OS_Mac + return macSetURLHandler(); +#else + return 0; #endif } @@ -429,10 +476,12 @@ int deleteURIprotocol() { system("cls"); printf("Program unregistered successfully!\n"); return 0; -#else +#elif OS_Linux // Do nothing on linux printf("A SGDB URL argument is required.\nExample: SGDBoop sgdb://boop/[ASSET_TYPE]/[ASSET_ID]\n"); return 1; +#elif OS_Mac + return 0; #endif } @@ -442,6 +491,11 @@ char* getSteamBaseDir() { char* steamBaseDir = malloc(MAX_PATH); int foundValue = 0; + if (getenv("BOOP_STEAMPATH") != NULL && strlen(getenv("BOOP_STEAMPATH")) > 0) { + strcpy(steamBaseDir, getenv("BOOP_STEAMPATH")); + return steamBaseDir; + } + #if OS_Windows char* steamPath = getWindowsRegistryString(HKEY_CURRENT_USER, "Software\\Valve\\Steam", "SteamPath"); @@ -449,7 +503,7 @@ char* getSteamBaseDir() { foundValue = 1; strcpy(steamBaseDir, steamPath); } -#else +#elif OS_Linux foundValue = 1; strcpy(steamBaseDir, getenv("HOME")); @@ -464,6 +518,10 @@ char* getSteamBaseDir() { // Steam installed on host strcat(steamBaseDir, "/.steam/steam"); } +#elif OS_Mac + foundValue = 1; + strcpy(steamBaseDir, getenv("HOME")); + strcat(steamBaseDir, "/Library/Application Support/Steam"); #endif if (foundValue < 1) { @@ -475,7 +533,7 @@ char* getSteamBaseDir() { } // Find the most recently logged-in user -char* getMostRecentUser(char* steamBaseDir) { +char* getMostRecentUserEx(char* steamBaseDir, int failOnExit) { char* tempSteamid = malloc(512); char* steamid = malloc(512); @@ -493,7 +551,11 @@ char* getMostRecentUser(char* steamBaseDir) { if (fp == NULL) { free(steamid); free(tempSteamid); - exitWithError("Couldn't find logged in user", 95); + if (failOnExit) { + exitWithError("Couldn't find logged in user", 95); + } else { + return NULL; + } } while ((read = readLine(&line, &len, fp)) != -1) { @@ -520,19 +582,24 @@ char* getMostRecentUser(char* steamBaseDir) { fclose(fp); if (line) free(line); + free(tempSteamid); return steamid; } +char* getMostRecentUser(char* steamBaseDir) { + return getMostRecentUserEx(steamBaseDir, 1); +} + // Get Steam's destination directory based on artwork type -char* getSteamDestinationDir(char* type, struct nonSteamApp* nonSteamAppData) { +char* getSteamDestinationDir(char* type, struct AppStruct* appData) { char* steamBaseDir = getSteamBaseDir(); if (steamBaseDir == NULL) { return NULL; } - if (strcmp(type, "icon") == 0 && nonSteamAppData == NULL) { + if (strcmp(type, "icon") == 0 && appData == NULL) { // If it's a Steam app icon strcat(steamBaseDir, "/appcache/librarycache/"); } @@ -548,9 +615,7 @@ char* getSteamDestinationDir(char* type, struct nonSteamApp* nonSteamAppData) { return steamBaseDir; } -// Get source mods appids -struct nonSteamApp* getSourceMods(const char* type) -{ +char* getModsPath(const char* type) { int goldsource = 0; if (strcmp(type, "goldsource") == 0) { goldsource = 1; @@ -592,17 +657,33 @@ struct nonSteamApp* getSourceMods(const char* type) strcat(regValueTemp, regValue); strcpy(regValue, regValueTemp); + // Registry is one dir up from the steam directory char* regFileLocation = getSteamBaseDir(); - regFileLocation = strreplace(regFileLocation, "/.steam/steam", "/.steam/registry.vdf"); + char* regFileLocationTemp = regFileLocation; + while (*(regFileLocationTemp + 6) != '\x0') { + regFileLocationTemp = strstr_i(regFileLocationTemp, "/steam"); + } + *regFileLocationTemp = '\x0'; + strcat(regFileLocation, "/registry.vdf"); fp_reg = fopen(regFileLocation, "r"); // If the file doesn't exist, skip this function if (fp_reg == NULL) { - char errorMessage[500]; - sprintf(errorMessage, "File registry.vdf could not be found in %s", regFileLocation); - free(regFileLocation); - logError(errorMessage, 96); - return NULL; + + // Try the old directory, inside the "steam" directory + regFileLocation = getSteamBaseDir(); + strcat(regFileLocation, "/registry.vdf"); + fp_reg = fopen(regFileLocation, "r"); + + + // If the old file also doesn't exist, just ignore this type of mods + if (fp_reg == NULL) { + char errorMessage[500]; + sprintf(errorMessage, "File registry.vdf could not be found in %s", regFileLocation); + free(regFileLocation); + logMessage(errorMessage, 96); + return NULL; + } } free(regFileLocation); @@ -630,22 +711,44 @@ struct nonSteamApp* getSourceMods(const char* type) if (foundValue) { sourceModPath = strreplace(sourceModPath, "\\\\", "/"); } -#endif +#endif if (!foundValue) { // Allow this to fail silently, the registry key doesn't always need to be there return NULL; } + return sourceModPath; +} + +// Get source mods appids +struct AppStruct* getSourceMods(const char* type) +{ + int goldsource = 0; + if (strcmp(type, "goldsource") == 0) { + goldsource = 1; + } + + char* sourceModPath = getModsPath(type); + + if (sourceModPath == NULL) + { + free(sourceModPath); + return NULL; + } + + char debug_message[1024]; + if (DEBUG) { + sprintf(debug_message, "Opening mods path: %s\n", sourceModPath); + logMessage(debug_message, 0); + } + struct dirent* dir; DIR* dr = opendir(sourceModPath); if (dr == NULL) { - char errorMessage[500]; - sprintf(errorMessage, "Could not read directory %s", sourceModPath); free(sourceModPath); - logError(errorMessage, 98); return NULL; } @@ -654,7 +757,7 @@ struct nonSteamApp* getSourceMods(const char* type) char** sourceModsNames = malloc(sizeof(char*) * 500000); int* sourceModsSteamAppIds = malloc(sizeof(char*) * 500000); char** sourceModsDirs = malloc(sizeof(char*) * 500000); - unsigned long modsCount = 0; + unsigned int modsCount = 0; while ((dir = readdir(dr)) != NULL) { @@ -688,12 +791,28 @@ struct nonSteamApp* getSourceMods(const char* type) int foundGameKey = 0; + if (DEBUG) { + sprintf(debug_message, "Reading file: %s\n", filepath); + logMessage(debug_message, 0); + } + while ((read = readLine(&line, &len, fp)) != -1) { // If line contains the "game" key, get the mod's name and create the struct entry unsigned char* commentChar = strstr(line, "//"); - unsigned char* nameStartChar = strstr(line, "game"); - unsigned char* steamAppIdStartChar = strstr(line, "SteamAppId"); + unsigned char* nameStartChar = strstr_i(line, "game"); + unsigned char* steamAppIdStartChar = strstr_i(line, "SteamAppId"); + + + if (DEBUG && nameStartChar > 0) { + sprintf(debug_message, "Found nameStartChar: %s\n", nameStartChar); + logMessage(debug_message, 0); + } + + if (DEBUG && steamAppIdStartChar > 0) { + sprintf(debug_message, "Found steamAppIdStartChar: %s\n", steamAppIdStartChar); + logMessage(debug_message, 0); + } // Make sure to first capture the "game" key, properly if (nameStartChar > 0 && commentChar == 0 && !foundGameKey && ((char *) nameStartChar == line || isspace(*(nameStartChar - 1))) && isspace(*(nameStartChar + 4))) { @@ -775,9 +894,14 @@ struct nonSteamApp* getSourceMods(const char* type) _sourceModsCount = modsCount; } - struct nonSteamApp* sourceMods = malloc(sizeof(nonSteamApp) * modsCount); + struct AppStruct* sourceMods = malloc(sizeof(AppStruct) * modsCount); + + if (DEBUG) { + sprintf(debug_message, "Found %u mods inside getSourceMods(%s)\n", modsCount, type); + logMessage(debug_message, 0); + } - for (int i = 0; i < modsCount; i++) { + for (unsigned int i = 0; i < modsCount; i++) { sourceMods[i].index = _modsCount; char int_string[50]; unsigned int hex_index = i; @@ -811,12 +935,224 @@ struct nonSteamApp* getSourceMods(const char* type) return sourceMods; } +uint32_t* getOwnedAppids(unsigned int* count) { + + *count = 0; + uint32_t* ownedAppids = malloc(sizeof(uint32_t)); + + // Get the shortcuts.vdf file + char* packageinfoVdfPath = getSteamBaseDir(); + strcat(packageinfoVdfPath, "/appcache/packageinfo.vdf"); + + // Parse the file + FILE* fp; + unsigned char buf[2] = { 0 }; + size_t bytes = 0; + size_t read = sizeof buf; + fp = fopen(packageinfoVdfPath, "rb"); + free(packageinfoVdfPath); + if (fp != NULL) { + fseek(fp, 0L, SEEK_END); + size_t filesize = ftell(fp) + 2; + fseek(fp, 0, SEEK_SET); + + unsigned char* fileContent = malloc(filesize + 1); + memset(fileContent, '\xAB', filesize); + unsigned int currentFileByte = 0; + + // Load the vdf in memory + while ((bytes = fread(buf, sizeof * buf, read, fp)) == read) { + for (int i = 0; i < sizeof buf; i++) { + fileContent[currentFileByte] = buf[i]; + currentFileByte++; + } + } + + fclose(fp); + const unsigned char* parsingChar = fileContent; + uint32_t appIdInt; + const unsigned char* appidBlockStart, *nextAppidBlock, *appId; + char message[512]; + + // Parse the vdf content + while ((size_t)(parsingChar - fileContent) < filesize) { + + // Find appids block + appidBlockStart = sgdb_memmem(parsingChar, filesize - (parsingChar - fileContent), "appids\x00\x02\x30\x00", 10) + 7; + if ((size_t)(appidBlockStart - fileContent) > filesize) { + break; + } + nextAppidBlock = sgdb_memmem(appidBlockStart, filesize - (appidBlockStart - fileContent), "appids\x00\x02\x30\x00", 10); + if (nextAppidBlock - fileContent > filesize) { + nextAppidBlock = fileContent + filesize; + } + + unsigned int appIdCount = 0; + unsigned char appIdIndex[10]; + + appId = appidBlockStart; + while (appId[0] == 0x02 && sprintf(appIdIndex, "%d", appIdCount) && startsWith((appId + 1), appIdIndex)) { + + appId += 2 + strlen(appIdIndex); + appIdInt = (uint32_t)appId[0] | + ((uint32_t)appId[1] << 8) | + ((uint32_t)appId[2] << 16) | + ((uint32_t)appId[3] << 24); + + ownedAppids = realloc(ownedAppids, sizeof(uint32_t) * ((*count) + 1)); + ownedAppids[(*count)++] = appIdInt; + + appId += 4; + appIdCount++; + + if (DEBUG) { + sprintf(message, "Found owned AppID: %d\n", appIdInt); + logMessage(message, 0); + } + } + + parsingChar = nextAppidBlock; + } + + free(fileContent); + } + + if (DEBUG) { + char message[512]; + sprintf(message, "Total owned apps: %d\n", *count); + logMessage(message, 0); + } + + return ownedAppids; + +} + +// Parse appinfo file and return a pointer to a list of structs containing the app data +struct AppStruct* getSteamApps() { + + unsigned int ownedAppidsCount = 0; + uint32_t* ownedAppids = getOwnedAppids(&ownedAppidsCount); + char* appinfoVdfPath = getSteamBaseDir(); + struct AppStruct* apps = malloc(sizeof(AppStruct)); + + // Get the shortcuts.vdf file + strcat(appinfoVdfPath, "/appcache/appinfo.vdf"); + + // Parse the file + FILE* fp; + unsigned char buf[2] = {0}; + size_t bytes = 0; + size_t read = sizeof buf; + fp = fopen(appinfoVdfPath, "rb"); + free(appinfoVdfPath); + if (fp != NULL) { + fseek(fp, 0L, SEEK_END); + size_t filesize = ftell(fp) + 2; + fseek(fp, 0, SEEK_SET); + + unsigned char* fileContent = malloc(filesize + 1); + memset(fileContent, '\xAB', filesize); + unsigned int currentFileByte = 0; + + // Load the vdf in memory + while ((bytes = fread(buf, sizeof * buf, read, fp)) == read) { + for (int i = 0; i < sizeof buf; i++) { + fileContent[currentFileByte] = buf[i]; + currentFileByte++; + } + } + + fclose(fp); + const unsigned char* parsingChar = fileContent; + uint32_t appIdInt; + unsigned char appidString[32]; + const unsigned char *appIdStart, *appIdEnd, *appNameStart, * appStoreName, *nextAppId, *gameTypeMatch; + char message[1024]; + + // Parse the vdf content + while ((size_t)(parsingChar - fileContent) < filesize) { + + // Find app name and appid + appIdStart = sgdb_memmem(parsingChar, filesize - (parsingChar - fileContent), "\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00", 10) + 10; + if ((size_t)(appIdStart - fileContent) > filesize) { + break; + } + nextAppId = sgdb_memmem(appIdStart, filesize - (appIdStart - fileContent), "\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00", 10) + 10; + if (nextAppId - fileContent > filesize) { + nextAppId = fileContent + filesize; + } + appIdInt = (uint32_t)appIdStart[0] | + ((uint32_t)appIdStart[1] << 8) | + ((uint32_t)appIdStart[2] << 16) | + ((uint32_t)appIdStart[3] << 24); + sprintf(appidString, "%u", appIdInt); + + appNameStart = sgdb_memmem(appIdStart, nextAppId - appIdStart, "\x01\x04\x00\x00\x00", 5) + 5; + + if ((size_t)appNameStart == 5) { + parsingChar = appIdStart; + continue; + } + + // Check if it's game type + gameTypeMatch = sgdb_memmem(appNameStart, nextAppId - appNameStart, "\x01\x05\x00\x00\x00game", 9) + 9; + if (gameTypeMatch > nextAppId || (size_t)gameTypeMatch == 9) { + gameTypeMatch = sgdb_memmem(appNameStart, nextAppId - appNameStart, "\x01\x05\x00\x00\x00Game", 9) + 9; + } + + appStoreName = sgdb_memmem(appNameStart, nextAppId - appNameStart, "\x07\x02\x00\x00\x01\x91\x01\x00\x00", 9) + 9; + if ((size_t)appStoreName != 9 && appStoreName < nextAppId) { + appNameStart = appStoreName; + } + + if (gameTypeMatch < nextAppId && (size_t)gameTypeMatch != 9) { + + // Check if app is owned + uint8_t owned = FALSE; + for (unsigned int i = 0; i < ownedAppidsCount; i++) { + if (ownedAppids[i] == appIdInt) { + owned = TRUE; + break; + } + } + + // Add app to array if it's owned + if (owned) { + // Realloc apps + apps = realloc(apps, sizeof(AppStruct) * (_steamAppsCount + 1)); + + // Add the values to struct + apps[_steamAppsCount].index = _steamAppsCount; + strcpy(apps[_steamAppsCount].name, appNameStart); + strcpy(apps[_steamAppsCount].appid, appidString); + strcpy(apps[_steamAppsCount].appid_old, appidString); + strcpy(apps[_steamAppsCount].type, "steam-app"); + ++_steamAppsCount; + + if (DEBUG) { + sprintf(message, "Found app: [%s] \"%s\"\n", appidString, appNameStart); + logMessage(message, 0); + } + } + } + + parsingChar = appNameStart + strlen(appNameStart); + } + + printf("%d\n", _steamAppsCount); + free(fileContent); + free(ownedAppids); + } + + return apps; +} + // Parse shortcuts file and return a pointer to a list of structs containing the app data -struct nonSteamApp* getNonSteamApps() { +struct AppStruct* getNonSteamApps() { char* shortcutsVdfPath = getSteamBaseDir(); char* steamid = getMostRecentUser(shortcutsVdfPath); - struct nonSteamApp* apps = malloc(sizeof(nonSteamApp) * 1500000); + struct AppStruct* apps = malloc(sizeof(AppStruct)); crcInit(); // Get the shortcuts.vdf file @@ -913,6 +1249,9 @@ struct nonSteamApp* getNonSteamApps() { *exeEndChar = '\x03'; + // Realloc apps + apps = realloc(apps, sizeof(AppStruct) * (_nonSteamAppsCount + 1)); + // Do math magic. Valve pls fix appid = (((appid | 0x80000000) << 32) | 0x02000000) >> 32; appid_old = (((appid_old | 0x80000000) << 32) | 0x02000000); @@ -923,7 +1262,8 @@ struct nonSteamApp* getNonSteamApps() { strcpy(apps[_nonSteamAppsCount].name, nameStartChar); sprintf(apps[_nonSteamAppsCount].appid, "%lu", (unsigned long)appid); strcpy(apps[_nonSteamAppsCount].type, "nonsteam-app"); - _nonSteamAppsCount++; + ++_nonSteamAppsCount; + // Move parser to end of app data *nameEndChar = 0x03; // Revert name string to prevent string-related problems @@ -937,11 +1277,28 @@ struct nonSteamApp* getNonSteamApps() { return apps; } -struct nonSteamApp* getMods() { - struct nonSteamApp* apps = malloc(sizeof(nonSteamApp) * 1500000); - struct nonSteamApp* sourceMods = getSourceMods("source"); - struct nonSteamApp* goldSourceMods = getSourceMods("goldsource"); - for (int i = 0; i < _sourceModsCount; i++) { +struct AppStruct* getMods() { + if (DEBUG) logMessage("Running getSourceMods(\"source\")", 0); + struct AppStruct* sourceMods = getSourceMods("source"); + + if (DEBUG) logMessage("Running getSourceMods(\"goldsource\")", 0); + struct AppStruct* goldSourceMods = getSourceMods("goldsource"); + + struct AppStruct* apps = malloc(sizeof(AppStruct) * (_sourceModsCount + _goldSourceModsCount)); + + char debug_message[1024]; + if (DEBUG) { + sprintf(debug_message, "Found %u source mods and %u goldsrc mods\n", _sourceModsCount, _goldSourceModsCount); + logMessage(debug_message, 0); + } + + for (unsigned int i = 0; i < _sourceModsCount; i++) { + + if (DEBUG) { + sprintf(debug_message, "Adding source mod #%u: %s\n", i, sourceMods[i].name); + logMessage(debug_message, 0); + } + apps[_modsCount].index = _modsCount; strcpy(apps[_modsCount].name, sourceMods[i].name); strcpy(apps[_modsCount].appid_old, sourceMods[i].appid_old); @@ -950,7 +1307,13 @@ struct nonSteamApp* getMods() { _modsCount++; } - for (int i = 0; i < _goldSourceModsCount; i++) { + for (unsigned int i = 0; i < _goldSourceModsCount; i++) { + + if (DEBUG) { + sprintf(debug_message, "Adding source mod #%u: %s\n", i, sourceMods[i].name); + logMessage(debug_message, 0); + } + apps[_modsCount].index = _modsCount; strcpy(apps[_modsCount].name, goldSourceMods[i].name); strcpy(apps[_modsCount].appid_old, goldSourceMods[i].appid_old); @@ -965,24 +1328,28 @@ struct nonSteamApp* getMods() { return apps; } -// Select a non-steam app from a dropdown list and return its ID -struct nonSteamApp* selectNonSteamApp(char* sgdbName, struct nonSteamApp* apps, struct nonSteamApp* appsMods) { +// Select an app from a dropdown list and return its ID +struct AppStruct* selectApp(char* sgdbName, struct AppStruct* appsNonSteam, struct AppStruct* appsMods, struct AppStruct* appsSteam) { - char* appid = malloc(128); - - char** values = malloc(sizeof(char*) * _nonSteamAppsCount); - for (int i = 0; i < _nonSteamAppsCount; i++) { - values[i] = malloc(strlen(apps[i].name) + 1); - strcpy(values[i], apps[i].name); + char** nonSteamValues = malloc(sizeof(char*) * _nonSteamAppsCount); + for (unsigned int i = 0; i < _nonSteamAppsCount; i++) { + nonSteamValues[i] = malloc(strlen(appsNonSteam[i].name) + 1); + strcpy(nonSteamValues[i], appsNonSteam[i].name); } char** modsValues = malloc(sizeof(char*) * _modsCount); - for (int i = 0; i < _modsCount; i++) { + for (unsigned int i = 0; i < _modsCount; i++) { modsValues[i] = malloc(strlen(appsMods[i].name) + 1); strcpy(modsValues[i], appsMods[i].name); } - struct nonSteamApp* appData = malloc(sizeof(nonSteamApp)); + char** steamValues = malloc(sizeof(char*) * _steamAppsCount); + for (unsigned int i = 0; i < _steamAppsCount; i++) { + steamValues[i] = malloc(strlen(appsSteam[i].name) + 1); + strcpy(steamValues[i], appsSteam[i].name); + } + + struct AppStruct* appData = malloc(sizeof(AppStruct)); // Create title string char* title = malloc(40 + strlen(sgdbName)); @@ -991,13 +1358,14 @@ struct nonSteamApp* selectNonSteamApp(char* sgdbName, struct nonSteamApp* apps, strcat(title, "'"); // Sort values - qsort(values, _nonSteamAppsCount, sizeof(const char*), compareStrings); + qsort(nonSteamValues, _nonSteamAppsCount, sizeof(const char*), compareStrings); + qsort(steamValues, _steamAppsCount, sizeof(const char*), compareStrings); qsort(modsValues, _modsCount, sizeof(const char*), compareStrings); // Find matching value int selection = -1; for (int i = 0; i < _nonSteamAppsCount; i++) { - if (strcmp_i(values[i], sgdbName) == 0) { + if (strcmp_i(nonSteamValues[i], sgdbName) == 0) { selection = i; break; } @@ -1013,14 +1381,16 @@ struct nonSteamApp* selectNonSteamApp(char* sgdbName, struct nonSteamApp* apps, } // Create selection dialog - int retval = SelectionDialog(title, _nonSteamAppsCount, (const char**)values, _modsCount, (const char**)modsValues, selection); + int retval = SelectionDialog(title, _nonSteamAppsCount, (const char**)nonSteamValues, _modsCount, (const char**)modsValues, _steamAppsCount, (const char**)steamValues, selection); // Exit when user clicks cancel if (retval < 0) { - free(apps); - free(values); + free(appsNonSteam); + free(nonSteamValues); free(appsMods); free(modsValues); + free(appsSteam); + free(steamValues); free(title); exit(0); } @@ -1028,17 +1398,23 @@ struct nonSteamApp* selectNonSteamApp(char* sgdbName, struct nonSteamApp* apps, // Find match int matchCount; - struct nonSteamApp * matchedStruct; + struct AppStruct * matchedStruct; char ** matchedValues; - if (retval >= _nonSteamAppsCount) { + if (retval >= _nonSteamAppsCount + _modsCount) { + retval -= _nonSteamAppsCount + _modsCount; + matchedStruct = appsSteam; + matchedValues = steamValues; + matchCount = _steamAppsCount; + } else if (retval >= _nonSteamAppsCount) { retval -= _nonSteamAppsCount; matchedStruct = appsMods; matchedValues = modsValues; + matchCount = _modsCount; } else { - matchedStruct = apps; - matchedValues = values; + matchedStruct = appsNonSteam; + matchedValues = nonSteamValues; matchCount = _nonSteamAppsCount; - } + } for (int i = 0; i < matchCount; i++) { if (strcmp(matchedStruct[i].name, matchedValues[retval]) == 0) { @@ -1051,33 +1427,19 @@ struct nonSteamApp* selectNonSteamApp(char* sgdbName, struct nonSteamApp* apps, } } - free(apps); - free(values); + free(appsNonSteam); + free(nonSteamValues); free(appsMods); free(modsValues); + free(appsSteam); + free(steamValues); free(title); return appData; } -// Create a symlink for a file that has the old nonsteam appid format -int createOldIdSymlink(struct nonSteamApp* appData, char* steamDestDir) { - char linkPath[MAX_PATH]; - char targetPath[MAX_PATH]; - - strcpy(linkPath, steamDestDir); - strcat(linkPath, appData->appid_old); - strcat(linkPath, ".jpg"); - - strcpy(targetPath, steamDestDir); - strcat(targetPath, appData->appid); - strcat(targetPath, ".jpg"); - - return symlink(targetPath, linkPath); -} - // Update shortcuts.vdf with the new icon value -void updateVdf(struct nonSteamApp* appData, char* filePath) { +void updateVdf(struct AppStruct* appData, char* filePath) { char* shortcutsVdfPath = getSteamBaseDir(); char* steamid = getMostRecentUser(shortcutsVdfPath); @@ -1191,8 +1553,27 @@ void updateVdf(struct nonSteamApp* appData, char* filePath) { } } +char* getExecutableDirectory(char* executablePath) { + char separator[2]; +#if defined(OS_Windows) + strcpy(separator, "\\"); +#else + strcpy(separator, "/"); +#endif + + char* executableDir = malloc(MAX_PATH), *executableDirTemp = executableDir; + strcpy(executableDir, executablePath); + + while (strstr(executableDirTemp, separator)) { + executableDirTemp = strstr(executableDirTemp, separator) + 1; + } + + *executableDirTemp = '\x0'; + return executableDir; +} + // Build as Windows app on windows instead of console - #if OS_Windows +#if OS_Windows int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow) { int argc = __argc; @@ -1201,10 +1582,42 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmd #else int main(int argc, char** argv) { - #endif +#endif + +#ifdef OS_Mac + // if param is not passed directly we're probs being launched via url + if (!startsWith(argv[argc > 1 ? 1 : 0], "sgdb://")) { + const char* aeUrl = macAwaitEvent(); + if (aeUrl != NULL) { + // override argv if launched via event + static char* fakeArgv[2]; + // The first param is always the binary, second is the sgdb:// + fakeArgv[0] = argv[0]; + fakeArgv[1] = (char*)aeUrl; + argc = 2; + argv = fakeArgv; + } + } +#endif + + // Check for debug enablers + DEBUG = getenv("SGDBOOP_DEBUG") != NULL && !strcmp(getenv("SGDBOOP_DEBUG"), "1"); + char debugFilePath[MAX_PATH]; + strcpy(debugFilePath, getExecutableDirectory(argv[0])); + strcat(debugFilePath, "/SGDBOOP_DEBUG"); + FILE* f = fopen(debugFilePath, "r"); + if (f != NULL) { + DEBUG = 1; + fclose(f); + } + + if (DEBUG) logMessage("Enabled debugging log", 0); // If no arguments were given, register the program if (argc == 0 || (argc == 1 && !startsWith(argv[0], "sgdb://"))) { + + if (DEBUG) logMessage("Attempting to register URI protocol", 0); + // Create the sgdb URI protocol if (createURIprotocol() == 1) { exitWithError("Could not create URI protocol.", 80); @@ -1214,6 +1627,9 @@ int main(int argc, char** argv) // If argument is unregister, unregister and exit if (strcmp(argv[1], "unregister") == 0) { + + if (DEBUG) logMessage("Attempting to unregister URI protocol", 0); + if (deleteURIprotocol() == 1) { exitWithError("Could not unregister the URI protocol.", 85); } @@ -1229,15 +1645,62 @@ int main(int argc, char** argv) // Test mode if (strcmp(argv[1], "sgdb://boop/test") == 0) { + + if (DEBUG) logMessage("Attempting to show test popup, starting with getSteamBaseDir.", 0); + char* steamBaseDir = getSteamBaseDir(); + + if (DEBUG) logMessage("Running getLogFilepath", 0); + char* logFilepath = getLogFilepath(); + + if (DEBUG) logMessage("Running getMostRecentUserEx", 0); + char* mostRecentUser = getMostRecentUserEx(steamBaseDir, 0); + + if (DEBUG) logMessage("Running getModsPath(\"source\")", 0); + char* sourceModPath = getModsPath("source"); + + if (DEBUG) logMessage("Running getModsPath(\"goldsource\")", 0); + char* goldSrcModPath = getModsPath("goldsource"); + + if (DEBUG) logMessage("Prepping test message", 0); + + char* message = malloc(1024); + strcpy(message, "(ノ◕ヮ◕)ノ*:・゚✧ SGDBoop is working! ★・゚:*ヽ(◕ヮ◕ヽ)\n\n"); + strcat(message, "Version: " VERSION "\n"); + strcat(message, "\nLast logged in user: "); + strcat(message, mostRecentUser != NULL ? mostRecentUser : "(not found)"); + strcat(message, "\n\n"); + strcat(message, "Steam directory:\n"); + strcat(message, steamBaseDir != NULL ? steamBaseDir : "(not found)"); + strcat(message, "\n\n"); + strcat(message, "Source mods:\n"); + strcat(message, sourceModPath != NULL ? sourceModPath : "(not found)"); + strcat(message, "\n\n"); + strcat(message, "GoldSrc mods:\n"); + strcat(message, goldSrcModPath != NULL ? goldSrcModPath : "(not found)"); + strcat(message, "\n\n"); + strcat(message, "Log file:\n"); + strcat(message, logFilepath); + strcat(message, "\n\nDebug logging: "); + strcat(message, DEBUG ? "Enabled" : "Disabled"); + + if (DEBUG) logMessage("Showing test message", 0); + // Show a message #if OS_Windows - ShowMessageBoxW(L"SGDBoop Test", L"(ノ◕ヮ◕)ノ*:・゚✧ SGDBoop is working! ★・゚:*ヽ(◕ヮ◕ヽ)"); + ShowMessageBoxW(L"SGDBoop Test", ConvertStringToUnicode(message)); #else - ShowMessageBox("SGDBoop Test", "(ノ◕ヮ◕)ノ*:・゚✧ SGDBoop is working! ★・゚:*ヽ(◕ヮ◕ヽ)"); + ShowMessageBox("SGDBoop Test", message); #endif + + if (DEBUG) logMessage("Showed test message", 0); + + free(message); return 0; } + + if (DEBUG) logMessage("Extracting params from argument.", 0); + // Get the params from the string char* types = strstr(argv[1], "sgdb://boop/") + strlen("sgdb://boop/"); char* grid_ids = strstr(types, "/"); @@ -1254,60 +1717,91 @@ int main(int argc, char** argv) strcpy(mode, "default"); } + if (DEBUG) logMessage("Getting asset URL.", 0); + // Get asset URL char*** apiValues = callAPI(types, grid_ids, mode); if (apiValues == NULL) { exitWithError("API didn't return an appropriate response.", 82); } - // Use the same nonSteamApp object for all calls - struct nonSteamApp* nonSteamAppData = NULL; + // Use the same app object for all calls + struct AppStruct* appData = NULL; + + if (DEBUG) logMessage("Running main boop loop", 0); for (int line = 0; line < _apiReturnedLines; line++) { + if (DEBUG) logMessage("Extracting api response values", 0); + char* app_id = apiValues[line][0]; char* orientation = apiValues[line][1]; char* assetUrl = apiValues[line][2]; char* asset_type = apiValues[line][3]; char* asset_hash = apiValues[line][4]; + if (DEBUG) { + logMessage("Extracting api response values", 0); + char message[1024]; + sprintf(message, "app_id: %s, asset_type: %s, orientation: %s, assetUrl: %s, asset_hash: %s\n", app_id, asset_type, orientation, assetUrl, asset_hash); + logMessage(message, 0); + } // If the game is a non-steam app, select an imported app if (startsWith(app_id, "nonsteam-")) { + + if (DEBUG) logMessage("Found non-steam appid", 0); + // Select app once if (line < 1) { // Do not include mods in the dropdown list if the only asset selected was an icon - int includeMods = TRUE; + uint8_t includeMods = TRUE; if (strcmp(types, "icon") == 0 || (strcmp(types, "steam") == 0 && strcmp(asset_type, "icon") == 0)) { includeMods = FALSE; } - // Get non-steam apps - struct nonSteamApp* apps = getNonSteamApps(); - struct nonSteamApp* appsMods = NULL; + // Get apps + if (DEBUG) logMessage("Running getNonSteamApps", 0); + struct AppStruct* appsNonSteam = getNonSteamApps(); + + if (DEBUG) logMessage("Running getSteamApps", 0); + struct AppStruct* appsSteam = getSteamApps(); + + struct AppStruct* appsMods = NULL; if (includeMods) { + if (DEBUG) logMessage("Running getMods", 0); appsMods = getMods(); } + if (DEBUG) { + logMessage("Assessing found apps", 0); + char message[1024]; + sprintf(message, "_nonSteamAppsCount: %u, _modsCount: %u, _steamAppsCount: %u\n", _nonSteamAppsCount, _modsCount, _steamAppsCount); + logMessage(message, 0); + } + // Exit with an error if nothing found - if (_nonSteamAppsCount < 1 && _modsCount < 1) { + if (_nonSteamAppsCount < 1 && _modsCount < 1 && _steamAppsCount < 1) { ShowMessageBox("SGDBoop Error", "Could not find any non-Steam apps or mods."); - free(apps); + free(appsNonSteam); + free(appsSteam); free(appsMods); exitWithError("Could not find any non-Steam apps or mods.", 91); } + if (DEBUG) logMessage("Running selectApp", 0); + // Show selection screen and return the struct - nonSteamAppData = selectNonSteamApp(strstr(app_id, "-") + 1, apps, appsMods); + appData = selectApp(strstr(app_id, "-") + 1, appsNonSteam, appsMods, appsSteam); } // Skip icons for source/goldsource mods if (strcmp(asset_type, "icon") == 0 && - (strcmp(nonSteamAppData->type, "source-mod") == 0 || strcmp(nonSteamAppData->type, "goldsource-mod") == 0)) { + (strcmp(appData->type, "source-mod") == 0 || strcmp(appData->type, "goldsource-mod") == 0)) { continue; } - app_id = nonSteamAppData->appid; + app_id = appData->appid; } else if (strcmp(asset_type, "icon") == 0 && strcmp(asset_hash, "notfound") == 0) { // If the requested asset is an official Steam icon that was not found in the api, skip this asset @@ -1315,22 +1809,31 @@ int main(int argc, char** argv) } // Get Steam base dir - char* steamDestDir = getSteamDestinationDir(asset_type, nonSteamAppData); + if (DEBUG) logMessage("Running getSteamDestinationDir", 0); + char* steamDestDir = getSteamDestinationDir(asset_type, appData); if (steamDestDir == NULL) { exitWithError("Could not locate Steam destination directory.", 83); } // Download asset file - char* outfilename = downloadAssetFile(app_id, assetUrl, asset_type, orientation, asset_hash, steamDestDir, nonSteamAppData); + if (DEBUG) { + logMessage("Preparing to download asset.", 0); + char message[1024]; + sprintf(message, "app_id: %s, asset_type: %s, orientation: %s, assetUrl: %s, asset_hash: %s, steamDestDir: %s\n", app_id, asset_type, orientation, assetUrl, asset_hash, steamDestDir); + logMessage(message, 0); + } + + char* outfilename = downloadAssetFile(app_id, assetUrl, asset_type, orientation, asset_hash, steamDestDir, appData); if (outfilename == NULL) { exitWithError("Could not download asset file.", 84); } // Non-Steam specific actions - if (nonSteamAppData) { + if (appData) { // If the asset is a non-Steam icon, add the path to the vdf if (strcmp(asset_type, "icon") == 0) { - updateVdf(nonSteamAppData, outfilename); + if (DEBUG) logMessage("Running updateVdf", 0); + updateVdf(appData, outfilename); } } @@ -1338,7 +1841,9 @@ int main(int argc, char** argv) free(steamDestDir); } - free(nonSteamAppData); + if (DEBUG) logMessage("Finished main boop loop.", 0); + + free(appData); } return 0; diff --git a/string-helpers.c b/string-helpers.c index e5efe63..1b1887e 100644 --- a/string-helpers.c +++ b/string-helpers.c @@ -5,60 +5,95 @@ #if defined(_WIN32) || defined(WIN32) #define strcasecmp _stricmp -#endif -// Overriding getline_custom -// https://stackoverflow.com/a/3501681/16642426 -size_t readLine(char** lineptr, size_t* n, FILE* stream) { - char* bufptr = NULL; - char* p = bufptr; - size_t size; - int c; +/* +getline() shim https://stackoverflow.com/a/47229318 - if (lineptr == NULL) { - return -1; - } - if (stream == NULL) { - return -1; - } - if (n == NULL) { - return -1; - } - bufptr = *lineptr; - size = *n; +The original code is public domain -- Will Hartung 4/9/2009 +Modifications, public domain as well, by Antti Haapala, 11/10/2017 + - Switched to getc on 5/23/2019 + - Proper error handling on IO error and wraparound + check on buffer extension 3/31/2025 - 4/2/2025 +*/ +#include +#include +#define MINIMUM_BUFFER_SIZE 128 - c = fgetc(stream); - if (c == EOF) { - return -1; - } - if (bufptr == NULL) { - bufptr = malloc(128); - if (bufptr == NULL) { - return -1; - } - size = 128; - } - p = bufptr; - while (c != EOF) { - if ((p - bufptr) > (size - 1)) { - size = size + 128; - bufptr = realloc(bufptr, size); - if (bufptr == NULL) { - return -1; - } - } - *p++ = c; - if (c == '\n') { - break; - } - c = fgetc(stream); - } +// if typedef doesn't exist (msvc, blah) +typedef intptr_t ssize_t; + +ssize_t getline(char **lineptr, size_t *n, FILE *stream) { + size_t pos; + int c; + + if (lineptr == NULL || stream == NULL || n == NULL) { + errno = EINVAL; + return -1; + } + + c = getc(stream); + if (c == EOF) { + return -1; + } + + if (*lineptr == NULL) { + *lineptr = malloc(MINIMUM_BUFFER_SIZE); + if (*lineptr == NULL) { + return -1; + } + *n = MINIMUM_BUFFER_SIZE; + } + + pos = 0; + while(c != EOF) { + if (pos + 1 >= *n) { + size_t new_size = *n + (*n >> 2); - *p++ = '\0'; - *lineptr = bufptr; - *n = size; + // have some reasonable minimum + if (new_size < MINIMUM_BUFFER_SIZE) { + new_size = MINIMUM_BUFFER_SIZE; + } + + // size_t wraparound + if (new_size <= *n) { + errno = ENOMEM; + return -1; + } - return p - bufptr - 1; + // Note you might also want to check that PTRDIFF_MAX + // is not exceeded! + + char *new_ptr = realloc(*lineptr, new_size); + if (new_ptr == NULL) { + return -1; + } + *n = new_size; + *lineptr = new_ptr; + } + + ((unsigned char *)(*lineptr))[pos ++] = c; + if (c == '\n') { + break; + } + c = getc(stream); + } + + (*lineptr)[pos] = '\0'; + + // if an IO error occurred, return -1 + if (c == EOF && !feof(stream)) { + return -1; + } + + // otherwise we successfully read until the end-of-file + // or the delimiter + return pos; +} +#endif + +size_t readLine(char** lineptr, size_t* n, FILE* stream) { + ssize_t result = getline(lineptr, n, stream); + return (result < 0) ? (size_t)-1 : (size_t)result; } // Check if a string starts with a given substring @@ -128,4 +163,24 @@ int strcmp_i(const char * a, const char * b) if (d != 0 || !*a) return d; } -} \ No newline at end of file +} + +// Custom memmem +// https://stackoverflow.com/a/52989329 +const unsigned char* sgdb_memmem(const void* haystack, size_t haystack_len, + const void* const needle, const size_t needle_len) +{ + if (haystack == NULL) return NULL; + if (haystack_len == 0) return NULL; + if (needle == NULL) return NULL; + if (needle_len == 0) return NULL; + + for (const unsigned char* h = haystack; + haystack_len >= needle_len; + ++h, --haystack_len) { + if (!memcmp(h, needle, needle_len)) { + return h; + } + } + return NULL; +} diff --git a/string-helpers.h b/string-helpers.h index 270053c..0984b64 100644 --- a/string-helpers.h +++ b/string-helpers.h @@ -2,8 +2,6 @@ #include #include -// Overriding getline_custom -// https://stackoverflow.com/a/3501681/16642426 size_t readLine(char**, size_t*, FILE*); // Check if a string starts with a given substring @@ -22,4 +20,8 @@ char* strreplace(char*, const char*, const char*); // Case-insensitive string comparison // https://stackoverflow.com/a/5820991/16642426 -int strcmp_i(const char*, const char*); \ No newline at end of file +int strcmp_i(const char*, const char*); + +// Custom memmem +// https://stackoverflow.com/a/52989329 +const unsigned char* sgdb_memmem(const void*, size_t, const void* const, const size_t); \ No newline at end of file