mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-02 08:18:14 -07:00
Compare commits
83 Commits
26-07-15
...
eaab4698eb
| Author | SHA1 | Date | |
|---|---|---|---|
| eaab4698eb | |||
| a48eccab93 | |||
| 81c913726d | |||
| 0169fe1e31 | |||
| 357e6c86f1 | |||
| 67963389ef | |||
| 4009bb8a84 | |||
| 8fb6d94000 | |||
| 8d3685415d | |||
| 50b38b856f | |||
| dad88caf01 | |||
| 699659d4bf | |||
| 21e7d24ed3 | |||
| 1cdb7be810 | |||
| 09314f1fd3 | |||
| e7b90271ca | |||
| 95f10ed733 | |||
| d51de976b1 | |||
| 8acd433ec6 | |||
| b9c8afbbc1 | |||
| 7d5f528e1a | |||
| 2dc3c0cbe4 | |||
| 4c2a70bab6 | |||
| 46f76597fc | |||
| b23640222c | |||
| 3f6862908e | |||
| 0934cce225 | |||
| 7c50fcc79e | |||
| 0f4ab63101 | |||
| 8b2f38307b | |||
| 3863d5a4ed | |||
| 94574c485a | |||
| f857926ec3 | |||
| adf4cccd4a | |||
| bf8e194685 | |||
| a2e508208c | |||
| f5888609a8 | |||
| c6cd72c528 | |||
| 82e0c053d0 | |||
| 3b29227dbc | |||
| ce2f93d234 | |||
| 13a171f199 | |||
| 3da352488e | |||
| 2386047c2f | |||
| baa550037a | |||
| b53447bed5 | |||
| 4959a58de3 | |||
| 9141ff453b | |||
| f955ab984c | |||
| 435b9ce9aa | |||
| 1ec7528dac | |||
| e7822aad8e | |||
| 8140e83e46 | |||
| cc0899290e | |||
| 3012139028 | |||
| c590b87cff | |||
| 5cabd026ae | |||
| 7ccd938f9b | |||
| 88a210e82e | |||
| 74e619df37 | |||
| 10a97b9c63 | |||
| 159043803c | |||
| bd2fbfcb67 | |||
| 64600826b7 | |||
| 80deb4bbbd | |||
| 00c8bb8ce6 | |||
| 09ed8b948c | |||
| e2daefbb6a | |||
| 39bf17aafe | |||
| 4836cd94ab | |||
| 9d41ca2515 | |||
| 18421ffbfe | |||
| 3f0921d983 | |||
| ed7318c270 | |||
| 623e1e3998 | |||
| 558e3d0cb3 | |||
| d0db547dd0 | |||
| be4bb00390 | |||
| a8cc85531c | |||
| 2dea0d27bf | |||
| 61a0220c1a | |||
| c080bbe301 | |||
| e1d1b39567 |
@@ -0,0 +1,46 @@
|
||||
# Normalize every text file to LF in the repository.
|
||||
# The build runs under Linux/MinGW containers, so LF is also used in the
|
||||
# working tree; Windows editors and toolchains handle LF fine.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Windows-only files that must keep CRLF in the working tree.
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.sln text eol=crlf
|
||||
*.vcproj text eol=crlf
|
||||
*.vcxproj text eol=crlf
|
||||
*.props text eol=crlf
|
||||
*.filters text eol=crlf
|
||||
|
||||
# Files that must keep LF even if a Windows editor rewrites them.
|
||||
*.sh text eol=lf
|
||||
*.in text eol=lf
|
||||
*.cmake text eol=lf
|
||||
*.mk text eol=lf
|
||||
Makefile text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
|
||||
# Binary files - never touch the contents.
|
||||
*.bin binary
|
||||
*.ico binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.bmp binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.gz binary
|
||||
*.dll binary
|
||||
*.exe binary
|
||||
*.lib binary
|
||||
*.a binary
|
||||
*.o binary
|
||||
*.obj binary
|
||||
*.pdb binary
|
||||
|
||||
# Vendored code is stored and checked out byte-for-byte as upstream ships it,
|
||||
# so re-importing a library never produces line-ending-only diffs.
|
||||
src/spice2x/external/** -text
|
||||
+31
-31
@@ -1,32 +1,32 @@
|
||||
on: [push, pull_request]
|
||||
name: Continuous Integration
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fw-ci:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set ccache environment variables
|
||||
run: |
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||
- name: Install ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
- name: Calculate commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short ${{ github.sha }})
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||
path: src/spice2x/bin
|
||||
on: [push, pull_request]
|
||||
name: Continuous Integration
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fw-ci:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set ccache environment variables
|
||||
run: |
|
||||
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
|
||||
- name: Install ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2.23
|
||||
- name: Calculate commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
calculatedSha=$(git rev-parse --short ${{ github.sha }})
|
||||
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||
path: src/spice2x/bin
|
||||
if-no-files-found: error
|
||||
@@ -1,49 +1,49 @@
|
||||
name: Draft Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Draft Release
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Clean leftover build artifacts
|
||||
run: |
|
||||
rm -rf .ccache dist bin cmake-build-*
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- name: Determine release name from dist filename
|
||||
run: |
|
||||
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
|
||||
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
|
||||
name="${dist%.zip}"
|
||||
# the tag is the date portion, e.g. 26-06-28
|
||||
tag="${name#spice2x-}"
|
||||
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
name: ${{ env.RELEASE_NAME }}
|
||||
target_commitish: main
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
src/spice2x/dist/spice2x-*.zip
|
||||
name: Draft Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Draft Release
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Clean leftover build artifacts
|
||||
run: |
|
||||
rm -rf .ccache dist bin cmake-build-*
|
||||
- name: Compile
|
||||
run: ./build_docker.sh
|
||||
- name: Determine release name from dist filename
|
||||
run: |
|
||||
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
|
||||
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
|
||||
name="${dist%.zip}"
|
||||
# the tag is the date portion, e.g. 26-06-28
|
||||
tag="${name#spice2x-}"
|
||||
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
draft: true
|
||||
prerelease: true
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
name: ${{ env.RELEASE_NAME }}
|
||||
target_commitish: main
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
src/spice2x/dist/spice2x-*.zip
|
||||
|
||||
@@ -22,6 +22,7 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
|
||||
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
|
||||
* Changes to the software license terms.
|
||||
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
|
||||
* Bundling third party DLL binary; e.g., Discord Social SDK, PC light control integration, and so on. These should be built as hook DLLs instead, optionally with [Spice SDK](https://github.com/spice2x/spice2x.github.io/wiki/Spice-SDK).
|
||||
|
||||
### Avoiding regressions
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve
|
||||
|
||||
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
|
||||
|
||||
### AI-Use Policy
|
||||
|
||||
Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewer’s time.
|
||||
|
||||
## Additional information
|
||||
|
||||
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
|
||||
|
||||
@@ -254,8 +254,60 @@ add_subdirectory(external/discord-rpc EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/hash-library EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/wslay EXCLUDE_FROM_ALL)
|
||||
add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL)
|
||||
|
||||
# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own
|
||||
# sysroot and cannot see it, so those targets build without JPEG support.
|
||||
add_library(spice_jpeg INTERFACE)
|
||||
if(NOT SPICE_XP)
|
||||
# search static archives only: the mingw package also ships an import library,
|
||||
# and linking that one would pull in a libjpeg DLL at runtime
|
||||
set(SPICE_JPEG_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})
|
||||
find_package(JPEG)
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_JPEG_SUFFIXES})
|
||||
|
||||
if(JPEG_FOUND)
|
||||
target_link_libraries(spice_jpeg INTERFACE JPEG::JPEG)
|
||||
target_compile_definitions(spice_jpeg INTERFACE SPICE_JPEG=1)
|
||||
else()
|
||||
message(WARNING
|
||||
"libjpeg-turbo not found: screen capture over the API is disabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# x264 for the API H.264 video stream, installed into the mingw sysroots by the
|
||||
# deps image. The WinXP toolchains deliberately go without it and serve MJPEG only.
|
||||
add_library(spice_x264 INTERFACE)
|
||||
if(NOT SPICE_XP)
|
||||
# search static archives only: the mingw package also ships an import library,
|
||||
# and linking that one would pull in a libx264 DLL at runtime
|
||||
set(SPICE_X264_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})
|
||||
find_library(X264_LIBRARY NAMES x264 libx264)
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_X264_SUFFIXES})
|
||||
|
||||
find_path(X264_INCLUDE_DIR NAMES x264.h)
|
||||
if(X264_LIBRARY AND X264_INCLUDE_DIR)
|
||||
target_include_directories(spice_x264 INTERFACE "${X264_INCLUDE_DIR}")
|
||||
target_link_libraries(spice_x264 INTERFACE "${X264_LIBRARY}")
|
||||
target_compile_definitions(spice_x264 INTERFACE SPICE_H264=1)
|
||||
else()
|
||||
message(WARNING
|
||||
"x264 not found: the api video stream will only offer MJPEG")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# fpng's SIMD needs the whole unit built for SSE4.1, which its runtime CPU check
|
||||
# cannot undo, so keep it scalar rather than raising the CPU baseline
|
||||
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
|
||||
COMPILE_DEFINITIONS "FPNG_NO_SSE=1")
|
||||
if(NOT MSVC)
|
||||
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
|
||||
COMPILE_OPTIONS "-fno-strict-aliasing")
|
||||
endif()
|
||||
|
||||
# set link time optimizations (disabled for Debug builds for speed, disabled
|
||||
# for RelWithDebInfo builds due to "lto1: error: two or more sections for"
|
||||
# errors)
|
||||
@@ -317,6 +369,10 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
# api
|
||||
api/controller.cpp
|
||||
api/websocket.cpp
|
||||
api/capture_pump.cpp
|
||||
api/h264_stream.cpp
|
||||
api/stream_format.cpp
|
||||
api/stream_server.cpp
|
||||
api/request.cpp
|
||||
api/response.cpp
|
||||
api/module.cpp
|
||||
@@ -332,6 +388,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
api/modules/control.cpp
|
||||
api/modules/touch.cpp
|
||||
api/modules/iidx.cpp
|
||||
api/modules/sdvx.cpp
|
||||
api/serial.cpp
|
||||
api/modules/drs.cpp
|
||||
api/modules/lcd.cpp
|
||||
@@ -379,7 +436,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
external/tinyxml2/tinyxml2.cpp
|
||||
external/http-parser/http_parser.c
|
||||
external/usbhidusage/usb-hid-usage.c
|
||||
external/toojpeg/toojpeg.cpp
|
||||
external/fpng/fpng.cpp
|
||||
external/scard/scard.cpp
|
||||
|
||||
# games
|
||||
@@ -411,12 +468,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/sdvx/sdvx_live2d.cpp
|
||||
games/sdvx/io.cpp
|
||||
games/sdvx/camera.cpp
|
||||
games/jb/bi2x_hook.cpp
|
||||
games/jb/jb.cpp
|
||||
games/jb/jb_touch.cpp
|
||||
games/jb/io.cpp
|
||||
games/nost/nost.cpp
|
||||
games/nost/io.cpp
|
||||
games/nost/poke.cpp
|
||||
games/nost/touch_mode.cpp
|
||||
games/gitadora/gitadora.cpp
|
||||
games/gitadora/asio.cpp
|
||||
games/gitadora/io.cpp
|
||||
@@ -461,6 +520,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/scotto/io.cpp
|
||||
games/drs/drs.cpp
|
||||
games/drs/io.cpp
|
||||
games/drs/motion_cam.cpp
|
||||
games/drs/rgb_cam.cpp
|
||||
games/we/we.cpp
|
||||
games/we/io.cpp
|
||||
@@ -472,6 +532,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
games/otoca/p4io.cpp
|
||||
games/silentscope/silentscope.cpp
|
||||
games/silentscope/io.cpp
|
||||
games/silentscope/projector.cpp
|
||||
games/pcm/pcm.cpp
|
||||
games/pcm/io.cpp
|
||||
games/onpara/onpara.cpp
|
||||
@@ -502,6 +563,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/audio/buffer.cpp
|
||||
hooks/audio/mme.cpp
|
||||
hooks/audio/util.cpp
|
||||
hooks/audio/xact.cpp
|
||||
hooks/audio/backends/dsound/dsound_backend.cpp
|
||||
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
|
||||
hooks/audio/backends/mmdevice/device.cpp
|
||||
@@ -528,10 +590,15 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/devicehook.cpp
|
||||
hooks/graphics/graphics.cpp
|
||||
hooks/graphics/graphics_windowed.cpp
|
||||
hooks/graphics/jpeg_encoder.cpp
|
||||
hooks/graphics/nvapi_impl.cpp
|
||||
hooks/graphics/nvapi_hook.cpp
|
||||
hooks/graphics/nvenc_hook.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_readback.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_gfdm.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
||||
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
||||
@@ -571,7 +638,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
misc/device.cpp
|
||||
misc/eamuse.cpp
|
||||
misc/extdev.cpp
|
||||
misc/nativetouchhook.cpp
|
||||
misc/hotkeys.cpp
|
||||
misc/sciunit.cpp
|
||||
misc/sde.cpp
|
||||
misc/wintouchemu.cpp
|
||||
@@ -609,17 +676,26 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
overlay/windows/keypad.cpp
|
||||
overlay/windows/log.cpp
|
||||
overlay/windows/midi.cpp
|
||||
overlay/windows/nostalgia_touch_piano.cpp
|
||||
overlay/windows/obs.cpp
|
||||
overlay/windows/obs_websocket.cpp
|
||||
overlay/windows/patch_manager.cpp
|
||||
overlay/windows/popn_sub.cpp
|
||||
overlay/windows/wnd_manager.cpp
|
||||
|
||||
# patcher
|
||||
patcher/config.cpp
|
||||
patcher/lifecycle.cpp
|
||||
patcher/loader.cpp
|
||||
patcher/loader_group.cpp
|
||||
patcher/runtime.cpp
|
||||
|
||||
# external
|
||||
external/easywsclient/easywsclient.cpp
|
||||
|
||||
# rawinput
|
||||
rawinput/rawinput.cpp
|
||||
rawinput/rawinput_handles.cpp
|
||||
rawinput/midi.cpp
|
||||
rawinput/sextet.cpp
|
||||
rawinput/piuio.cpp
|
||||
@@ -648,6 +724,11 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
|
||||
# touch
|
||||
touch/gdi_overlay.cpp
|
||||
touch/native/inject.cpp
|
||||
touch/native/inject_mouse.cpp
|
||||
touch/native/inject_synthetic.cpp
|
||||
touch/native/nativetouchhook.cpp
|
||||
touch/native/transform.cpp
|
||||
touch/touch.cpp
|
||||
touch/touch_gestures.cpp
|
||||
touch/win7.cpp
|
||||
@@ -719,7 +800,7 @@ endfunction()
|
||||
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
|
||||
target_link_libraries(spicetools_spice_objs
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
|
||||
|
||||
if(NOT MSVC)
|
||||
@@ -759,7 +840,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
|
||||
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_spice_linux
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
|
||||
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
|
||||
@@ -777,7 +858,7 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||
target_link_libraries(spicetools_spice64
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
||||
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
||||
@@ -800,7 +881,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||
target_link_libraries(spicetools_spice64_linux
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
||||
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
|
||||
@@ -819,9 +900,10 @@ endif()
|
||||
set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
|
||||
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
|
||||
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
# the configurator serves neither the API nor the video stream, so it needs no codecs
|
||||
target_link_libraries(spicetools_cfg
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
|
||||
target_link_libraries(spicetools_cfg PUBLIC winscard)
|
||||
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg")
|
||||
@@ -839,7 +921,7 @@ set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.
|
||||
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||
target_link_libraries(spicetools_cfg_linux
|
||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
|
||||
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
||||
target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
|
||||
|
||||
@@ -127,7 +127,30 @@ doesn't matter since the TCP protocol doesn't allow for out of order data,
|
||||
however this may change when/if support for UDP is being introduced. The only
|
||||
restriction is that the ID has to be a valid 64-bit unsigned integer.
|
||||
|
||||
#### Capture
|
||||
- get_screens()
|
||||
- returns the screen numbers the game has registered for capture
|
||||
- get_jpg(screen: uint, quality: uint, divide: uint)
|
||||
- returns the timestamp, width, height and base64 encoded JPEG of one screen
|
||||
- all parameters are optional and default to screen 0, quality 70, divide 1
|
||||
- divide shrinks the image by that factor before encoding
|
||||
- get_streams()
|
||||
- returns a dict describing the HTTP video stream, or no data at all when
|
||||
`-apistream` is not enabled and there is nothing to describe
|
||||
- `port` is the stream server port
|
||||
- `formats` lists the wire formats this build serves, each with a `name`
|
||||
(`h264` or `mjpeg`) and the `path` to request them on
|
||||
- `screens` lists every capturable screen with its `width`, `height`, and
|
||||
`busy`
|
||||
|
||||
#### Card
|
||||
- get_cards()
|
||||
- returns the current card ID and source for each active card reader
|
||||
- each entry contains `index`, `card_id`, and `source`
|
||||
- `source` is `file` or `override`
|
||||
- file entries also contain `file_name`, without the full configured path
|
||||
- `-card0` and `-card1` overrides do not contain `file_name`
|
||||
- this function only works when an API password is configured
|
||||
- insert(index: uint, card_id: hex)
|
||||
- inserts a card which gets read by the emulated card readers for the game
|
||||
- index has to be either 0 (for P1) or 1 (for P2)
|
||||
@@ -267,6 +290,20 @@ which also means that your hex edits are applicable directly.
|
||||
- `Side Panel Right Inner`
|
||||
- `Side Panel Right`
|
||||
|
||||
#### SDVX
|
||||
- tapeled_get(name: str, ...)
|
||||
- returns a list containing a dict of the current tape LED states. The dict keys are:
|
||||
- `Title`
|
||||
- `Upper Left Speaker`
|
||||
- `Upper Right Speaker`
|
||||
- `Left Wing`
|
||||
- `Right Wing`
|
||||
- `Control Panel`
|
||||
- `Lower Left Speaker`
|
||||
- `Lower Right Speaker`
|
||||
- `Woofer`
|
||||
- `V Unit`
|
||||
|
||||
#### LCD
|
||||
- info()
|
||||
- returns information about the serial LCD controller some games use
|
||||
@@ -277,6 +314,47 @@ which also means that your hex edits are applicable directly.
|
||||
- image_resize_set_scene(scene: int)
|
||||
- sets the active scene for image resize state; set to 0 to disable resize
|
||||
|
||||
## Video Stream
|
||||
|
||||
Separate from the JSON API, spice can serve the mirrored screen as a video
|
||||
stream over plain HTTP. Enable it with `-apistream`. It listens on the API port
|
||||
plus two, in the same way the WebSocket server uses the API port plus one, so
|
||||
`-api 1337` puts the stream on 1339. This means `-api` has to be enabled too.
|
||||
|
||||
Rather than working the port out, clients should ask the JSON API for it with
|
||||
`capture.get_streams()`, which also reports which of the formats below this
|
||||
build serves, the size of each screen and whether one is already taken.
|
||||
|
||||
Two formats are served:
|
||||
|
||||
http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace
|
||||
http://host:1339/stream.h264 H.264 annex-b, no container
|
||||
|
||||
All accept the same optional query parameters:
|
||||
|
||||
- `screen` - which screen to mirror, 0-3. Defaults to the subscreen when the
|
||||
game has one, otherwise the main screen.
|
||||
- `fps` - frames per second, 1-60. Default 30.
|
||||
- `q` - quality, 1-100. Default 70. This is the JPEG quality for `stream.mjpg`
|
||||
and is mapped onto the H.264 rate factor for `stream.h264`, so the same number
|
||||
does not mean the same thing for both.
|
||||
|
||||
For example:
|
||||
|
||||
http://host:1339/stream.h264?screen=1&fps=30&q=70
|
||||
|
||||
See the wiki for format tradeoffs, latency tuning, testing commands and client
|
||||
notes.
|
||||
|
||||
The stream is view only. Touch and other input still go through the JSON API,
|
||||
so a companion app needs both. There is no authentication on the stream port -
|
||||
anyone who can reach it can watch the screen.
|
||||
|
||||
WinXP builds have no video stream. Neither encoder is compiled in, so nothing
|
||||
listens on the stream port even with `-apistream`, `capture.get_streams()`
|
||||
returns no data, and the JSON API's JPEG screen capture is unavailable for the
|
||||
same reason.
|
||||
|
||||
## Native wrapper libraries
|
||||
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
|
||||
Python is the only one that is fully spec compliant.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "bmpu.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "acio/icca/icca.h"
|
||||
#include "avs/game.h"
|
||||
#include "cfg/api.h"
|
||||
@@ -9,6 +11,7 @@
|
||||
#include "games/ftt/io.h"
|
||||
#include "games/museca/io.h"
|
||||
#include "games/silentscope/io.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/eamuse.h"
|
||||
|
||||
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
|
||||
STATUS_BUFFER[4] |= 0x20;
|
||||
}
|
||||
|
||||
// joy stick raw input
|
||||
// gun position, sent big endian; the game keeps the top 12 bits of each pair
|
||||
auto &analogs = games::silentscope::get_analogs();
|
||||
auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X);
|
||||
auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y);
|
||||
unsigned short joy_x = 0x7FFF;
|
||||
unsigned short joy_y = 0x7FFF;
|
||||
if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) {
|
||||
joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX);
|
||||
}
|
||||
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
|
||||
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX);
|
||||
|
||||
if (gun_x.isSet() || gun_y.isSet()) {
|
||||
if (gun_x.isSet()) {
|
||||
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
|
||||
}
|
||||
if (gun_y.isSet()) {
|
||||
joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX);
|
||||
}
|
||||
} else {
|
||||
|
||||
// the gun reports where it is aimed, which a relative pointer cannot express - the
|
||||
// calibration screen asks for the screen centre and two corners, so read the cursor
|
||||
POINT cursor {};
|
||||
RECT client {};
|
||||
POINT origin {};
|
||||
if (NDD_MAIN_WINDOW != nullptr &&
|
||||
GetCursorPos(&cursor) &&
|
||||
GetClientRect(NDD_MAIN_WINDOW, &client) &&
|
||||
ClientToScreen(NDD_MAIN_WINDOW, &origin) &&
|
||||
client.right > 1 && client.bottom > 1)
|
||||
{
|
||||
const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1);
|
||||
const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1);
|
||||
joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1));
|
||||
joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// invert X axis
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "launcher/launcher.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
|
||||
// static stuff
|
||||
static int ACIO_WARMUP = 0;
|
||||
@@ -140,8 +141,9 @@ static int __cdecl ac_io_update(int a1) {
|
||||
// flush device output
|
||||
RI_MGR->devices_flush_output();
|
||||
|
||||
// update wintouchemu
|
||||
// update touch emulation
|
||||
wintouchemu::update();
|
||||
nativetouch::refresh_contact_lifetime();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "games/nost/io.h"
|
||||
#include "games/nost/nost.h"
|
||||
#include "games/nost/touch_mode.h"
|
||||
#include "util/logging.h"
|
||||
#include "avs/game.h"
|
||||
|
||||
@@ -16,6 +17,7 @@ using namespace GameAPI;
|
||||
// static stuff
|
||||
static uint8_t STATUS_BUFFER[277];
|
||||
static bool STATUS_BUFFER_FREEZE = false;
|
||||
static constexpr uint8_t NOST_TOUCH_PIANO_VELOCITY = 11;
|
||||
|
||||
/*
|
||||
* Implementations
|
||||
@@ -226,6 +228,8 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
games::nost::Analogs::Key27, games::nost::Analogs::Key28,
|
||||
};
|
||||
|
||||
const auto touch_key_state = games::nost::touch_mode::piano_key_state();
|
||||
|
||||
// iterate pairs of keys
|
||||
for (size_t key_pair = 0; key_pair < 28 / 2; key_pair++) {
|
||||
|
||||
@@ -257,6 +261,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button0 > 0) {
|
||||
state0 = button0;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2))) &&
|
||||
state0 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state0 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
const auto button1 = panb_get_button_velocity(
|
||||
buttons.at(button_mapping[key_pair * 2 + 1]),
|
||||
@@ -267,6 +275,10 @@ static bool __cdecl ac_io_panb_update_control_status_buffer() {
|
||||
if (button1 > 0) {
|
||||
state1 = button1;
|
||||
}
|
||||
if ((touch_key_state & (UINT32_C(1) << (key_pair * 2 + 1))) &&
|
||||
state1 < NOST_TOUCH_PIANO_VELOCITY) {
|
||||
state1 = NOST_TOUCH_PIANO_VELOCITY;
|
||||
}
|
||||
|
||||
// build value
|
||||
uint8_t value = 0;
|
||||
|
||||
@@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
|
||||
// SDVX Old cabinet mode
|
||||
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
|
||||
answer_type = 1;
|
||||
if (avs::game::is_model("L44"))
|
||||
if (avs::game::is_model({ "L44", "T44" }))
|
||||
answer_type = 2;
|
||||
|
||||
// check answer type
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "capture_pump.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include "hooks/graphics/graphics.h"
|
||||
|
||||
namespace api::capture_pump {
|
||||
|
||||
namespace {
|
||||
|
||||
std::array<std::mutex, GRAPHICS_CAPTURE_SCREEN_NO> CONSUMER_M;
|
||||
|
||||
// read once per capture from the present thread, so it stays lock free
|
||||
std::atomic<bool> CLAIMED[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||
|
||||
bool valid_screen(int screen) {
|
||||
return 0 <= screen && screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO);
|
||||
}
|
||||
}
|
||||
|
||||
bool capture_direct(int screen, std::shared_ptr<uint8_t[]> &out, int divide,
|
||||
uint64_t *timestamp, int *width, int *height) {
|
||||
|
||||
if (!valid_screen(screen)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(CONSUMER_M[screen]);
|
||||
graphics_capture_trigger(screen);
|
||||
return graphics_capture_receive_raw(
|
||||
screen, out, divide, timestamp, width, height);
|
||||
}
|
||||
|
||||
bool claim_screen(int screen) {
|
||||
if (!valid_screen(screen)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !CLAIMED[screen].exchange(true);
|
||||
}
|
||||
|
||||
void release_screen(int screen) {
|
||||
if (!valid_screen(screen)) {
|
||||
return;
|
||||
}
|
||||
|
||||
CLAIMED[screen] = false;
|
||||
}
|
||||
|
||||
bool screen_claimed(int screen) {
|
||||
if (!valid_screen(screen)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CLAIMED[screen];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace api::capture_pump {
|
||||
|
||||
struct Frame {
|
||||
// packed 24bpp RGB, width * height * 3 bytes
|
||||
std::shared_ptr<uint8_t[]> pixels;
|
||||
uint64_t timestamp = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// the graphics layer has one capture slot per screen, so concurrent waiters would steal
|
||||
// each other's frames; everything that captures goes through here to keep it serialized
|
||||
bool capture_direct(int screen, std::shared_ptr<uint8_t[]> &out, int divide,
|
||||
uint64_t *timestamp = nullptr, int *width = nullptr, int *height = nullptr);
|
||||
|
||||
// a screen carries one stream at a time; false when another connection already holds it
|
||||
bool claim_screen(int screen);
|
||||
void release_screen(int screen);
|
||||
|
||||
// true while a video stream client holds this screen
|
||||
bool screen_claimed(int screen);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace api {
|
||||
|
||||
extern std::atomic_uint32_t CLIENT_COUNT;
|
||||
|
||||
inline bool has_clients() {
|
||||
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "client.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "external/rapidjson/error/en.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
@@ -28,6 +30,7 @@
|
||||
#include "modules/lcd.h"
|
||||
#include "modules/lights.h"
|
||||
#include "modules/memory.h"
|
||||
#include "modules/sdvx.h"
|
||||
#include "modules/touch.h"
|
||||
#include "modules/resize.h"
|
||||
#include "request.h"
|
||||
@@ -36,6 +39,8 @@
|
||||
using namespace rapidjson;
|
||||
using namespace api;
|
||||
|
||||
std::atomic_uint32_t api::CLIENT_COUNT = 0;
|
||||
|
||||
Controller::Controller(unsigned short port, std::string password, bool pretty)
|
||||
: port(port), password(std::move(password)), pretty(pretty)
|
||||
{
|
||||
@@ -308,6 +313,8 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
|
||||
|
||||
// check for parse error
|
||||
if (document.HasParseError()) {
|
||||
log_warning("api", "Request JSON parse error: {} (byte offset {}, decrypted request length {} bytes)",
|
||||
GetParseError_En(document.GetParseError()), document.GetErrorOffset(), in_size);
|
||||
|
||||
// return empty response and close connection
|
||||
out->push_back(0);
|
||||
@@ -333,9 +340,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
|
||||
if (module->name == request.module) {
|
||||
module_found = true;
|
||||
|
||||
// check password force
|
||||
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
|
||||
Value err("Module requires the password to be set.");
|
||||
// check password requirement
|
||||
if (module->requires_password(request.function)
|
||||
&& this->password.empty()
|
||||
&& request.function != "session_refresh") {
|
||||
Value err("Function requires the password to be set.");
|
||||
response.add_error(err);
|
||||
break;
|
||||
}
|
||||
@@ -411,8 +420,11 @@ void Controller::init_state(api::ClientState *state) {
|
||||
state->modules.push_back(new modules::LCD());
|
||||
state->modules.push_back(new modules::Lights());
|
||||
state->modules.push_back(new modules::Memory());
|
||||
state->modules.push_back(new modules::SDVX());
|
||||
state->modules.push_back(new modules::Touch());
|
||||
state->modules.push_back(new modules::Resize());
|
||||
|
||||
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Controller::free_state(api::ClientState *state) {
|
||||
@@ -424,6 +436,8 @@ void Controller::free_state(api::ClientState *state) {
|
||||
|
||||
// free cipher
|
||||
delete state->cipher;
|
||||
|
||||
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Controller::free_socket() {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
#include "h264_stream.h"
|
||||
|
||||
#ifdef SPICE_H264
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <x264.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace {
|
||||
|
||||
// BT.601 limited range, the range every decoder assumes for H.264 without
|
||||
// explicit colour metadata
|
||||
inline uint8_t rgb_to_y(int r, int g, int b) {
|
||||
return static_cast<uint8_t>(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16);
|
||||
}
|
||||
|
||||
inline uint8_t rgb_to_u(int r, int g, int b) {
|
||||
return static_cast<uint8_t>(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128);
|
||||
}
|
||||
|
||||
inline uint8_t rgb_to_v(int r, int g, int b) {
|
||||
return static_cast<uint8_t>(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128);
|
||||
}
|
||||
|
||||
// a bare annex-b elementary stream, one encoder per connection so every client
|
||||
// starts on its own keyframe. no container, so nothing here keeps a media clock
|
||||
class H264Writer : public StreamWriter {
|
||||
public:
|
||||
|
||||
H264Writer(int quality, int fps) : quality(quality), fps(fps) {}
|
||||
|
||||
~H264Writer() override {
|
||||
this->close();
|
||||
}
|
||||
|
||||
std::string content_type() const override {
|
||||
return "video/h264";
|
||||
}
|
||||
|
||||
bool write(const StreamSend &send, const capture_pump::Frame &frame) override {
|
||||
|
||||
// I420 needs even dimensions
|
||||
const int width = frame.width & ~1;
|
||||
const int height = frame.height & ~1;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this->encoder == nullptr) {
|
||||
if (!this->open(width, height)) {
|
||||
return false;
|
||||
}
|
||||
} else if (width != this->width || height != this->height) {
|
||||
// the encoder is fixed at the size it opened with; let the client reconnect
|
||||
log_info("api::stream", "capture size changed, ending H.264 client");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->convert(frame.pixels.get(), frame.width);
|
||||
|
||||
this->picture.i_pts = this->frame_index;
|
||||
|
||||
x264_nal_t *nals = nullptr;
|
||||
int nal_count = 0;
|
||||
x264_picture_t picture_out;
|
||||
const int size = x264_encoder_encode(
|
||||
this->encoder, &nals, &nal_count, &this->picture, &picture_out);
|
||||
|
||||
if (size < 0) {
|
||||
log_warning("api::stream", "H.264 encode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->frame_index++;
|
||||
|
||||
if (size == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// x264 lays every NAL of the frame out back to back. an SEI or delimiter
|
||||
// carries no picture, so only the parameter sets and the slice go through
|
||||
this->annexb.clear();
|
||||
for (int i = 0; i < nal_count; i++) {
|
||||
switch (nals[i].i_type) {
|
||||
case NAL_SEI:
|
||||
case NAL_AUD:
|
||||
case NAL_FILLER:
|
||||
continue;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->annexb.insert(this->annexb.end(),
|
||||
nals[i].p_payload, nals[i].p_payload + nals[i].i_payload);
|
||||
}
|
||||
|
||||
if (this->annexb.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return send(this->annexb.data(), this->annexb.size());
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
bool open(int width, int height) {
|
||||
|
||||
x264_param_t param;
|
||||
if (x264_param_default_preset(¶m, "ultrafast", "zerolatency") < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
param.i_csp = X264_CSP_I420;
|
||||
param.i_width = width;
|
||||
param.i_height = height;
|
||||
param.i_fps_num = this->fps;
|
||||
param.i_fps_den = 1;
|
||||
|
||||
// sliced threading, which zerolatency already selected, so a frame is split
|
||||
// across workers rather than held back to be reordered. deliberately not the
|
||||
// automatic count: this shares a machine with the game it is capturing, and
|
||||
// taking every core to encode would win back frames at the game's expense
|
||||
param.i_threads = 4;
|
||||
|
||||
param.b_annexb = 1;
|
||||
// SPS/PPS ahead of every IDR, so a client can start decoding cold
|
||||
param.b_repeat_headers = 1;
|
||||
// a keyframe every two seconds bounds how long a new client waits
|
||||
param.i_keyint_max = this->fps * 2;
|
||||
param.i_log_level = X264_LOG_NONE;
|
||||
param.rc.i_rc_method = X264_RC_CRF;
|
||||
param.rc.f_rf_constant = 40.0f - (this->quality * 0.25f);
|
||||
|
||||
// baseline keeps hardware decode available on the widest range of phones
|
||||
if (x264_param_apply_profile(¶m, "baseline") < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->encoder = x264_encoder_open(¶m);
|
||||
if (this->encoder == nullptr) {
|
||||
log_warning("api::stream", "could not open the H.264 encoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x264_picture_alloc(&this->picture, X264_CSP_I420, width, height) < 0) {
|
||||
this->close();
|
||||
return false;
|
||||
}
|
||||
this->picture_ready = true;
|
||||
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (this->picture_ready) {
|
||||
x264_picture_clean(&this->picture);
|
||||
this->picture_ready = false;
|
||||
}
|
||||
if (this->encoder != nullptr) {
|
||||
x264_encoder_close(this->encoder);
|
||||
this->encoder = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// packed 24bpp RGB to I420, averaging each 2x2 block for the chroma planes
|
||||
void convert(const uint8_t *rgb, int source_width) {
|
||||
|
||||
uint8_t *plane_y = this->picture.img.plane[0];
|
||||
uint8_t *plane_u = this->picture.img.plane[1];
|
||||
uint8_t *plane_v = this->picture.img.plane[2];
|
||||
const int stride_y = this->picture.img.i_stride[0];
|
||||
const int stride_u = this->picture.img.i_stride[1];
|
||||
const int stride_v = this->picture.img.i_stride[2];
|
||||
|
||||
for (int y = 0; y < this->height; y++) {
|
||||
const uint8_t *row = rgb + static_cast<size_t>(y) * source_width * 3;
|
||||
uint8_t *out_y = plane_y + static_cast<size_t>(y) * stride_y;
|
||||
|
||||
for (int x = 0; x < this->width; x++) {
|
||||
const uint8_t *pixel = row + x * 3;
|
||||
out_y[x] = rgb_to_y(pixel[0], pixel[1], pixel[2]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int y = 0; y < this->height / 2; y++) {
|
||||
const uint8_t *row0 = rgb + static_cast<size_t>(y * 2) * source_width * 3;
|
||||
const uint8_t *row1 = row0 + static_cast<size_t>(source_width) * 3;
|
||||
uint8_t *out_u = plane_u + static_cast<size_t>(y) * stride_u;
|
||||
uint8_t *out_v = plane_v + static_cast<size_t>(y) * stride_v;
|
||||
|
||||
for (int x = 0; x < this->width / 2; x++) {
|
||||
const uint8_t *p00 = row0 + (x * 2) * 3;
|
||||
const uint8_t *p01 = p00 + 3;
|
||||
const uint8_t *p10 = row1 + (x * 2) * 3;
|
||||
const uint8_t *p11 = p10 + 3;
|
||||
|
||||
const int r = (p00[0] + p01[0] + p10[0] + p11[0] + 2) / 4;
|
||||
const int g = (p00[1] + p01[1] + p10[1] + p11[1] + 2) / 4;
|
||||
const int b = (p00[2] + p01[2] + p10[2] + p11[2] + 2) / 4;
|
||||
|
||||
out_u[x] = rgb_to_u(r, g, b);
|
||||
out_v[x] = rgb_to_v(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int quality;
|
||||
int fps;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int64_t frame_index = 0;
|
||||
std::vector<uint8_t> annexb;
|
||||
|
||||
x264_t *encoder = nullptr;
|
||||
x264_picture_t picture {};
|
||||
bool picture_ready = false;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<StreamWriter> make_h264_writer(int quality, int fps) {
|
||||
return std::make_unique<H264Writer>(quality, fps);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // SPICE_H264
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "stream_format.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
// bare annex-b H.264; null when the build has no encoder
|
||||
std::unique_ptr<StreamWriter> make_h264_writer(int quality, int fps);
|
||||
}
|
||||
@@ -16,6 +16,14 @@ namespace api {
|
||||
this->password_force = password_force;
|
||||
}
|
||||
|
||||
void Module::require_password(const std::string &function) {
|
||||
this->password_force_functions.emplace(function);
|
||||
}
|
||||
|
||||
bool Module::requires_password(const std::string &function) const {
|
||||
return this->password_force || this->password_force_functions.contains(function);
|
||||
}
|
||||
|
||||
void Module::handle(Request &req, Response &res) {
|
||||
|
||||
// log module access
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <external/robin_hood.h>
|
||||
|
||||
#include "response.h"
|
||||
@@ -26,6 +27,13 @@ namespace api {
|
||||
// default constructor
|
||||
explicit Module(std::string name, bool password_force=false);
|
||||
|
||||
void require_password(const std::string &function);
|
||||
|
||||
private:
|
||||
|
||||
// functions which expose sensitive data or actions
|
||||
std::unordered_set<std::string> password_force_functions;
|
||||
|
||||
public:
|
||||
|
||||
// virtual deconstructor
|
||||
@@ -35,6 +43,8 @@ namespace api {
|
||||
std::string name;
|
||||
bool password_force;
|
||||
|
||||
bool requires_password(const std::string &function) const;
|
||||
|
||||
// the magic
|
||||
void handle(Request &req, Response &res);
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
#include "capture.h"
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include "api/capture_pump.h"
|
||||
#include "api/stream_format.h"
|
||||
#include "api/stream_server.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "hooks/graphics/jpeg_encoder.h"
|
||||
#include "util/crypt.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
@@ -69,6 +74,7 @@ namespace api::modules {
|
||||
Capture::Capture() : Module("capture") {
|
||||
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
|
||||
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
|
||||
functions["get_streams"] = std::bind(&Capture::get_streams, this, _1, _2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +99,6 @@ namespace api::modules {
|
||||
* reduce: uint for dividing image size
|
||||
*/
|
||||
void Capture::get_jpg(Request &req, Response &res) {
|
||||
CAPTURE_BUFFER.clear();
|
||||
CAPTURE_BUFFER.reserve(1024 * 128);
|
||||
|
||||
// settings
|
||||
int screen = 0;
|
||||
@@ -120,10 +124,16 @@ namespace api::modules {
|
||||
uint64_t timestamp = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
graphics_capture_trigger(screen);
|
||||
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
|
||||
CAPTURE_BUFFER.push_back(byte);
|
||||
}, true, quality, true, divide, ×tamp, &width, &height);
|
||||
|
||||
std::shared_ptr<uint8_t[]> pixels;
|
||||
bool success = capture_pump::capture_direct(
|
||||
screen, pixels, divide, ×tamp, &width, &height);
|
||||
|
||||
if (success) {
|
||||
CAPTURE_BUFFER.clear();
|
||||
success = jpeg_encoder::encode(
|
||||
CAPTURE_BUFFER, pixels.get(), width, height, quality);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
|
||||
@@ -135,4 +145,98 @@ namespace api::modules {
|
||||
CAPTURE_BUFFER.clear();
|
||||
try_cached_response(screen, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* get_streams()
|
||||
*/
|
||||
void Capture::get_streams(Request &req, Response &res) {
|
||||
|
||||
auto &alloc = res.doc()->GetAllocator();
|
||||
|
||||
// nothing is listening without -apistream, so there is no stream to describe
|
||||
const unsigned short port = stream_server_port();
|
||||
if (port == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Value formats(kArrayType);
|
||||
for (const auto &[name, path] : stream_formats()) {
|
||||
Value entry(kObjectType);
|
||||
entry.AddMember("name", Value(name.c_str(), alloc), alloc);
|
||||
entry.AddMember("path", Value(path.c_str(), alloc), alloc);
|
||||
formats.PushBack(entry, alloc);
|
||||
}
|
||||
|
||||
std::vector<int> screen_numbers;
|
||||
graphics_screens_get(screen_numbers);
|
||||
|
||||
// measuring a screen nobody has captured yet waits for the game to present, which can
|
||||
// take as long as the whole request is allowed, so only one screen is measured per
|
||||
// call and the rest are reported null until a later one settles them. which screen
|
||||
// gets the attempt rotates, otherwise one that never presents would take every
|
||||
// request and the screens behind it would stay unmeasured forever
|
||||
int probe_screen = -1;
|
||||
{
|
||||
std::vector<int> unmeasured;
|
||||
for (const auto screen : screen_numbers) {
|
||||
if (screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
|
||||
&& !graphics_capture_last_size(screen, nullptr, nullptr)
|
||||
&& !capture_pump::screen_claimed(screen)) {
|
||||
unmeasured.push_back(screen);
|
||||
}
|
||||
}
|
||||
|
||||
if (!unmeasured.empty()) {
|
||||
static std::atomic<unsigned> probe_cursor { 0 };
|
||||
probe_screen = unmeasured[probe_cursor.fetch_add(1) % unmeasured.size()];
|
||||
}
|
||||
}
|
||||
|
||||
Value screens(kArrayType);
|
||||
for (const auto screen : screen_numbers) {
|
||||
if (screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool known = graphics_capture_last_size(screen, &width, &height);
|
||||
|
||||
// a probe holds the screen for as long as it waits, so a second caller arriving
|
||||
// during one would queue behind it and then take a wait of its own; let it report
|
||||
// the screen as unmeasured instead and pick the size up once the first is done
|
||||
static std::atomic<bool> probe_running { false };
|
||||
if (!known && screen == probe_screen && !probe_running.exchange(true)) {
|
||||
std::shared_ptr<uint8_t[]> pixels;
|
||||
known = capture_pump::capture_direct(
|
||||
screen, pixels, 1, nullptr, &width, &height);
|
||||
probe_running = false;
|
||||
}
|
||||
|
||||
// a screen of unknown size cannot be described, and a client told about it could
|
||||
// not size its decoder anyway; leaving it out until it has been measured beats
|
||||
// handing over an entry that has to be treated as absent
|
||||
if (!known) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Value entry(kObjectType);
|
||||
entry.AddMember("screen", screen, alloc);
|
||||
entry.AddMember("width", width, alloc);
|
||||
entry.AddMember("height", height, alloc);
|
||||
|
||||
// a screen carries one viewer at a time, so this is what decides whether a client
|
||||
// can connect at all; still racy by the time it does, only more honest than not
|
||||
entry.AddMember("busy", capture_pump::screen_claimed(screen), alloc);
|
||||
|
||||
screens.PushBack(entry, alloc);
|
||||
}
|
||||
|
||||
Value info(kObjectType);
|
||||
info.AddMember("port", port, alloc);
|
||||
info.AddMember("formats", formats, alloc);
|
||||
info.AddMember("screens", screens, alloc);
|
||||
|
||||
res.add_data(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@ namespace api::modules {
|
||||
// function definitions
|
||||
void get_screens(Request &req, Response &res);
|
||||
void get_jpg(Request &req, Response &res);
|
||||
void get_streams(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "card.h"
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "util/logging.h"
|
||||
@@ -11,8 +12,71 @@ using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
static bool normalize_card_id(const std::string &value, std::string &card_id) {
|
||||
if (value.size() != 16) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t card_bin[8] {};
|
||||
if (!hex2bin(value.c_str(), card_bin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
card_id = bin2hex(card_bin, std::size(card_bin));
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool read_card_id(const std::filesystem::path &path, std::string &card_id) {
|
||||
std::ifstream file(path);
|
||||
char buffer[16] {};
|
||||
if (!file.read(buffer, std::size(buffer))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalize_card_id(std::string(buffer, std::size(buffer)), card_id);
|
||||
}
|
||||
|
||||
Card::Card() : Module("card") {
|
||||
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
|
||||
functions["insert"] = std::bind(&Card::insert, this, _1, _2);
|
||||
require_password("get_cards");
|
||||
}
|
||||
|
||||
/**
|
||||
* get_cards()
|
||||
*/
|
||||
void Card::get_cards(Request &req, Response &res) {
|
||||
auto &alloc = res.doc()->GetAllocator();
|
||||
|
||||
for (int index = 0; index < eamuse_get_game_keypads(); index++) {
|
||||
std::string card_id;
|
||||
std::string filename;
|
||||
const auto card_override = eamuse_get_card_override(index);
|
||||
const bool has_override = !card_override.empty();
|
||||
|
||||
if (has_override) {
|
||||
if (!normalize_card_id(card_override, card_id)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
const auto path = eamuse_get_card_path(index);
|
||||
if (!read_card_id(path, card_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto filename_u8 = path.filename().u8string();
|
||||
filename.assign(filename_u8.begin(), filename_u8.end());
|
||||
}
|
||||
|
||||
Value card(kObjectType);
|
||||
card.AddMember("index", index, alloc);
|
||||
card.AddMember("card_id", Value(card_id.c_str(), alloc), alloc);
|
||||
card.AddMember("source", Value(has_override ? "override" : "file", alloc), alloc);
|
||||
if (!has_override) {
|
||||
card.AddMember("file_name", Value(filename.c_str(), alloc), alloc);
|
||||
}
|
||||
res.add_data(card);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace api::modules {
|
||||
private:
|
||||
|
||||
// function definitions
|
||||
void get_cards(Request &req, Response &res);
|
||||
void insert(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace api::modules {
|
||||
return error_type(res, "amount", "int");
|
||||
|
||||
// add to coin stock
|
||||
eamuse_coin_set_stock(eamuse_coin_get_stock() + std::max(0, req.params[0].GetInt()));
|
||||
eamuse_coin_add(std::max(0, req.params[0].GetInt()));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
#include "ddr.h"
|
||||
#include <functional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "games/ddr/ddr.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
DDR::DDR() : Module("ddr") {
|
||||
functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI
|
||||
*/
|
||||
void DDR::tapeled_get(Request &req, Response &res) {
|
||||
static const char* device_names[11] = {
|
||||
"p1_foot_up",
|
||||
"p1_foot_right",
|
||||
"p1_foot_left",
|
||||
"p1_foot_down",
|
||||
"p2_foot_up",
|
||||
"p2_foot_right",
|
||||
"p2_foot_left",
|
||||
"p2_foot_down",
|
||||
"top_panel",
|
||||
"monitor_left",
|
||||
"monitor_right"
|
||||
};
|
||||
|
||||
Value response_object(kObjectType);
|
||||
|
||||
// Iterate through each device and dump its lights data into the response
|
||||
for (size_t device = 0; device < 11; device++) {
|
||||
size_t num_leds = 25;
|
||||
if (device > 7)
|
||||
num_leds = 50;
|
||||
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(num_leds * 3, res.doc()->GetAllocator());
|
||||
for (size_t led = 0; led < num_leds; led++) {
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
res.add_data(response_object);
|
||||
}
|
||||
}
|
||||
#include "ddr.h"
|
||||
#include <functional>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "games/ddr/ddr.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
DDR::DDR() : Module("ddr") {
|
||||
functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI
|
||||
*/
|
||||
void DDR::tapeled_get(Request &req, Response &res) {
|
||||
static const char* device_names[11] = {
|
||||
"p1_foot_up",
|
||||
"p1_foot_right",
|
||||
"p1_foot_left",
|
||||
"p1_foot_down",
|
||||
"p2_foot_up",
|
||||
"p2_foot_right",
|
||||
"p2_foot_left",
|
||||
"p2_foot_down",
|
||||
"top_panel",
|
||||
"monitor_left",
|
||||
"monitor_right"
|
||||
};
|
||||
|
||||
Value response_object(kObjectType);
|
||||
|
||||
// Iterate through each device and dump its lights data into the response
|
||||
for (size_t device = 0; device < 11; device++) {
|
||||
size_t num_leds = 25;
|
||||
if (device > 7)
|
||||
num_leds = 50;
|
||||
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(num_leds * 3, res.doc()->GetAllocator());
|
||||
for (size_t led = 0; led < num_leds; led++) {
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator());
|
||||
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
res.add_data(response_object);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
class DDR : public Module {
|
||||
public:
|
||||
DDR();
|
||||
|
||||
private:
|
||||
// function definitions
|
||||
void tapeled_get(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
class DDR : public Module {
|
||||
public:
|
||||
DDR();
|
||||
|
||||
private:
|
||||
// function definitions
|
||||
void tapeled_get(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,14 +106,16 @@ namespace api::modules {
|
||||
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
|
||||
// Create an array for the light state
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator());
|
||||
light_state.Reserve(
|
||||
static_cast<SizeType>(mapping.data.size() * 3),
|
||||
res.doc()->GetAllocator());
|
||||
for (const auto [r, g, b] : mapping.data) {
|
||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||
light_state.PushBack(b, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
// Can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
// can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "keypads.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "util/precise_timer.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
@@ -59,7 +60,6 @@ namespace api::modules {
|
||||
// get params
|
||||
auto keypad = req.params[0].GetUint();
|
||||
auto input = std::string(req.params[1].GetString());
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// process all chars
|
||||
for (auto c : input) {
|
||||
@@ -93,11 +93,11 @@ namespace api::modules {
|
||||
|
||||
// set
|
||||
eamuse_set_keypad_overrides(keypad, state);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
|
||||
// unset
|
||||
eamuse_set_keypad_overrides(keypad, 0);
|
||||
timer.sleep(sleep_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "sdvx.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
using namespace std::placeholders;
|
||||
using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
SDVX::SDVX() : Module("sdvx") {
|
||||
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
|
||||
|
||||
for (auto &light : games::sdvx::TAPELED_MAPPING) {
|
||||
lights_by_names.emplace(light.lightName, light);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* tapeled_get()
|
||||
* tapeled_get(name: str, ...)
|
||||
*/
|
||||
void SDVX::tapeled_get(Request &req, Response &res) {
|
||||
Value response_object(kObjectType);
|
||||
|
||||
// all tape leds
|
||||
if (req.params.Size() == 0) {
|
||||
// iterate through each device and dump its lights data into the response
|
||||
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
|
||||
copy_tapeled_data(res, response_object, mapping);
|
||||
}
|
||||
} else {
|
||||
// specified light names
|
||||
for (Value ¶m : req.params.GetArray()) {
|
||||
// check params
|
||||
if (!param.IsString()) {
|
||||
error_type(res, "name", "string");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto name = param.GetString();
|
||||
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
|
||||
copy_tapeled_data(res, response_object, it->second.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.add_data(response_object);
|
||||
}
|
||||
|
||||
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
|
||||
const tapeledutils::tape_led &mapping)
|
||||
{
|
||||
Value light_state(kArrayType);
|
||||
light_state.Reserve(
|
||||
static_cast<SizeType>(mapping.data.size() * 3),
|
||||
res.doc()->GetAllocator());
|
||||
for (const auto [r, g, b] : mapping.data) {
|
||||
light_state.PushBack(r, res.doc()->GetAllocator());
|
||||
light_state.PushBack(g, res.doc()->GetAllocator());
|
||||
light_state.PushBack(b, res.doc()->GetAllocator());
|
||||
}
|
||||
|
||||
// can't use StringRef here, turns some strings partially into null bytes for some reason
|
||||
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
|
||||
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "external/robin_hood.h"
|
||||
#include "games/sdvx/sdvx.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
class SDVX : public Module {
|
||||
public:
|
||||
SDVX();
|
||||
|
||||
private:
|
||||
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
|
||||
|
||||
void tapeled_get(Request &req, Response &res);
|
||||
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
|
||||
const tapeledutils::tape_led &mapping);
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "touch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
#include "external/rapidjson/document.h"
|
||||
@@ -8,7 +10,11 @@
|
||||
#include "misc/eamuse.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "touch/touch.h"
|
||||
#include "touch/native/inject.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "touch/native/transform.h"
|
||||
#include "util/utils.h"
|
||||
#include "games/gitadora/gitadora.h"
|
||||
#include "games/iidx/iidx.h"
|
||||
|
||||
using namespace std::placeholders;
|
||||
@@ -17,6 +23,54 @@ using namespace rapidjson;
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
static bool inject_native_touch(
|
||||
const std::vector<TouchPoint> &touch_points, int canvas_w, int canvas_h) {
|
||||
if (touch_points.empty()) {
|
||||
// no active touches remain; release the synthetic contact
|
||||
return nativetouch::inject::inject_synthetic_touch({ 0, 0 }, false);
|
||||
}
|
||||
|
||||
// only a single synthetic contact is supported, so just use the first touch point
|
||||
const auto &touch = touch_points.front();
|
||||
POINT position { touch.x, touch.y };
|
||||
if (canvas_w > 0 && canvas_h > 0) {
|
||||
return nativetouch::inject::inject_synthetic_touch_from_canvas(
|
||||
position,
|
||||
{ canvas_w, canvas_h },
|
||||
true);
|
||||
}
|
||||
return nativetouch::inject::inject_synthetic_touch(position, true);
|
||||
}
|
||||
|
||||
// map API coordinates onto the touch space SDVX reads, which depends on how it is displayed
|
||||
static void sdvx_touch_errata(
|
||||
int &x, int &y, bool use_native, int canvas_w, int canvas_h) {
|
||||
|
||||
// windowed coordinates already match the sub screen window they land on
|
||||
if (GRAPHICS_WINDOWED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// landscape mode: native injection hands the game these coordinates
|
||||
// unchanged, so apply the rotation the touchscreen gets, while wintouchemu instead
|
||||
// rotates them later through the subscreen overlay
|
||||
if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
||||
if (use_native) {
|
||||
POINT position { x, y };
|
||||
if (nativetouch::transform::sdvx_landscape_rotate(&position, canvas_w, canvas_h)) {
|
||||
x = position.x;
|
||||
y = position.y;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// rotate into the portrait touch space
|
||||
const int x_raw = x;
|
||||
x = canvas_w - y;
|
||||
y = x_raw;
|
||||
}
|
||||
|
||||
Touch::Touch() : Module("touch") {
|
||||
is_sdvx = avs::game::is_model("KFC");
|
||||
|
||||
@@ -26,6 +80,32 @@ namespace api::modules {
|
||||
is_tdj_fhd = false;
|
||||
}
|
||||
|
||||
use_native = nativetouch::is_hooked();
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata)
|
||||
native_canvas_w = 0;
|
||||
native_canvas_h = 0;
|
||||
if (is_sdvx) {
|
||||
// windowed API coordinates land on the sub screen window as-is; fullscreen
|
||||
// coordinates are rotated into the game's touch space by apply_touch_errata
|
||||
const bool landscape_coordinates =
|
||||
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
|
||||
native_canvas_w = landscape_coordinates ? 1920 : 1080;
|
||||
native_canvas_h = landscape_coordinates ? 1080 : 1920;
|
||||
} else if (avs::game::is_model("LDJ")) {
|
||||
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
|
||||
native_canvas_w = is_tdj_fhd ? 1920 : 1280;
|
||||
native_canvas_h = is_tdj_fhd ? 1080 : 720;
|
||||
} else if (avs::game::is_model("M39")) {
|
||||
// pop'n music API touch surface
|
||||
native_canvas_w = 1280;
|
||||
native_canvas_h = 800;
|
||||
} else if (games::gitadora::is_arena_model()) {
|
||||
// GITADORA arena SMALL subscreen, either in its own window or in the overlay
|
||||
native_canvas_w = games::gitadora::ARENA_SUBSCREEN_WIDTH;
|
||||
native_canvas_h = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
|
||||
}
|
||||
|
||||
functions["read"] = std::bind(&Touch::read, this, _1, _2);
|
||||
functions["write"] = std::bind(&Touch::write, this, _1, _2);
|
||||
functions["write_reset"] = std::bind(&Touch::write_reset, this, _1, _2);
|
||||
@@ -99,7 +179,11 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// apply touch points
|
||||
touch_write_points(&touch_points);
|
||||
if (use_native) {
|
||||
write_native(touch_points);
|
||||
} else {
|
||||
touch_write_points(&touch_points);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,23 +207,54 @@ namespace api::modules {
|
||||
}
|
||||
|
||||
// remove all IDs
|
||||
touch_remove_points(&touch_point_ids);
|
||||
if (use_native) {
|
||||
write_reset_native(touch_point_ids);
|
||||
} else {
|
||||
touch_remove_points(&touch_point_ids);
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_native(const std::vector<TouchPoint> &touch_points) {
|
||||
if (touch_points.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
const auto touch_id = touch_points.front().id;
|
||||
if (native_touch_id && *native_touch_id != touch_id) {
|
||||
if (!inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
return;
|
||||
}
|
||||
native_touch_id.reset();
|
||||
}
|
||||
if (inject_native_touch(touch_points, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id = touch_id;
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::write_reset_native(const std::vector<DWORD> &touch_point_ids) {
|
||||
std::lock_guard<std::mutex> lock(native_touch_mutex);
|
||||
if (!native_touch_id ||
|
||||
std::find(
|
||||
touch_point_ids.begin(),
|
||||
touch_point_ids.end(),
|
||||
*native_touch_id) == touch_point_ids.end()) {
|
||||
return;
|
||||
}
|
||||
if (inject_native_touch({}, native_canvas_w, native_canvas_h)) {
|
||||
native_touch_id.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void Touch::apply_touch_errata(int &x, int &y) {
|
||||
int x_raw = x;
|
||||
int y_raw = y;
|
||||
|
||||
if (is_tdj_fhd) {
|
||||
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
|
||||
// we don't know what screen is being shown on the companion and the API doesn't specify
|
||||
// the target of the touch events so just assume it's the sub screen
|
||||
x = x_raw * 1920 / 1280;
|
||||
y = y_raw * 1080 / 720;
|
||||
x = x * 1920 / 1280;
|
||||
y = y * 1080 / 720;
|
||||
} else if (is_sdvx) {
|
||||
// for exceed gear, they are both 1080p screens, but need to apply transformation
|
||||
x = 1080 - y_raw;
|
||||
y = x_raw;
|
||||
sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include "api/module.h"
|
||||
#include "api/request.h"
|
||||
#include "touch/touch.h"
|
||||
|
||||
namespace api::modules {
|
||||
|
||||
@@ -13,11 +17,21 @@ namespace api::modules {
|
||||
private:
|
||||
bool is_sdvx;
|
||||
bool is_tdj_fhd;
|
||||
bool use_native;
|
||||
std::mutex native_touch_mutex;
|
||||
std::optional<uint32_t> native_touch_id;
|
||||
|
||||
// resolution the API/companion sends native touch coordinates in (after errata);
|
||||
// used to normalize before mapping into the native injection window
|
||||
int native_canvas_w;
|
||||
int native_canvas_h;
|
||||
|
||||
// function definitions
|
||||
void read(Request &req, Response &res);
|
||||
void write(Request &req, Response &res);
|
||||
void write_reset(Request &req, Response &res);
|
||||
void write_native(const std::vector<TouchPoint> &touch_points);
|
||||
void write_reset_native(const std::vector<DWORD> &touch_point_ids);
|
||||
void apply_touch_errata(int &x, int &y);
|
||||
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ part "src/exceptions.dart";
|
||||
part "src/rc4.dart";
|
||||
part "src/wrappers/analogs.dart";
|
||||
part "src/wrappers/buttons.dart";
|
||||
part "src/wrappers/capture.dart";
|
||||
part "src/wrappers/card.dart";
|
||||
part "src/wrappers/coin.dart";
|
||||
part "src/wrappers/control.dart";
|
||||
|
||||
@@ -4,8 +4,8 @@ part of spiceapi;
|
||||
class Connection {
|
||||
|
||||
// settings
|
||||
static const _TIMEOUT = Duration(seconds: 2);
|
||||
static const _BUFFER_SIZE = 1024 * 8;
|
||||
static const _TIMEOUT = Duration(seconds: 3);
|
||||
static const _BUFFER_SIZE = 1024 * 1024 * 8;
|
||||
|
||||
// state
|
||||
final String host, pass;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
part of spiceapi;
|
||||
|
||||
class CaptureData {
|
||||
int timestamp;
|
||||
int width, height;
|
||||
Uint8List data;
|
||||
}
|
||||
|
||||
var _base64DecoderInstance = Base64Decoder();
|
||||
|
||||
Future<List> captureGetScreens(Connection con) {
|
||||
var req = Request("capture", "get_screens");
|
||||
return con.request(req).then((res) {
|
||||
return res.getData();
|
||||
});
|
||||
}
|
||||
|
||||
Future<CaptureData> captureGetJPG(Connection con, {
|
||||
int screen = 0,
|
||||
int quality = 70,
|
||||
int divide = 1,
|
||||
}) {
|
||||
var req = Request("capture", "get_jpg");
|
||||
req.addParam(screen);
|
||||
req.addParam(quality);
|
||||
req.addParam(divide);
|
||||
return con.request(req).then((res) {
|
||||
var captureData = CaptureData();
|
||||
var data = res.getData();
|
||||
if (data.length > 0) captureData.timestamp = data[0];
|
||||
if (data.length > 1) captureData.width = data[1];
|
||||
if (data.length > 2) captureData.height = data[2];
|
||||
if (data.length > 3) {
|
||||
captureData.data = _base64DecoderInstance.convert(data[3]);
|
||||
}
|
||||
return captureData;
|
||||
});
|
||||
}
|
||||
|
||||
/// Describes the HTTP video stream, or null when this spice2x serves none.
|
||||
Future<Map> captureGetStreams(Connection con) {
|
||||
var req = Request("capture", "get_streams");
|
||||
return con.request(req).then((res) {
|
||||
var data = res.getData();
|
||||
return data.length > 0 ? data[0] : null;
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,32 @@
|
||||
part of spiceapi;
|
||||
|
||||
class CardInfo {
|
||||
final int index;
|
||||
final String cardID;
|
||||
final String source;
|
||||
final String? fileName;
|
||||
|
||||
CardInfo(this.index, this.cardID, this.source, this.fileName);
|
||||
}
|
||||
|
||||
Future<List<CardInfo>> cardGetCards(Connection con) {
|
||||
var req = Request("card", "get_cards");
|
||||
return con.request(req).then((res) {
|
||||
List<CardInfo> cards = [];
|
||||
for (var value in res.getData()) {
|
||||
cards.add(
|
||||
CardInfo(
|
||||
value["index"],
|
||||
value["card_id"],
|
||||
value["source"],
|
||||
value["file_name"],
|
||||
),
|
||||
);
|
||||
}
|
||||
return cards;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||
var req = Request("card", "insert");
|
||||
req.addParam(unit);
|
||||
|
||||
@@ -17,7 +17,7 @@ Future<List> captureGetScreens(Connection con) {
|
||||
|
||||
Future<CaptureData> captureGetJPG(Connection con, {
|
||||
int screen = 0,
|
||||
int quality = 60,
|
||||
int quality = 70,
|
||||
int divide = 1,
|
||||
}) {
|
||||
var req = Request("capture", "get_jpg");
|
||||
@@ -36,3 +36,12 @@ Future<CaptureData> captureGetJPG(Connection con, {
|
||||
return captureData;
|
||||
});
|
||||
}
|
||||
|
||||
/// Describes the HTTP video stream, or null when this spice2x serves none.
|
||||
Future<Map> captureGetStreams(Connection con) {
|
||||
var req = Request("capture", "get_streams");
|
||||
return con.request(req).then((res) {
|
||||
var data = res.getData();
|
||||
return data.length > 0 ? data[0] : null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
part of spiceapi;
|
||||
|
||||
class CardInfo {
|
||||
final int index;
|
||||
final String cardID;
|
||||
final String source;
|
||||
final String? fileName;
|
||||
|
||||
CardInfo(this.index, this.cardID, this.source, this.fileName);
|
||||
}
|
||||
|
||||
Future<List<CardInfo>> cardGetCards(Connection con) {
|
||||
var req = Request("card", "get_cards");
|
||||
return con.request(req).then((res) {
|
||||
List<CardInfo> cards = [];
|
||||
for (var value in res.getData()) {
|
||||
cards.add(
|
||||
CardInfo(
|
||||
value["index"],
|
||||
value["card_id"],
|
||||
value["source"],
|
||||
value["file_name"],
|
||||
),
|
||||
);
|
||||
}
|
||||
return cards;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||
var req = Request("card", "insert");
|
||||
req.addParam(unit);
|
||||
|
||||
@@ -2,11 +2,13 @@ from .connection import Connection
|
||||
from .request import Request
|
||||
from .analogs import *
|
||||
from .buttons import *
|
||||
from .capture import *
|
||||
from .card import *
|
||||
from .coin import *
|
||||
from .control import *
|
||||
from .exceptions import *
|
||||
from .iidx import *
|
||||
from .sdvx import *
|
||||
from .info import *
|
||||
from .keypads import *
|
||||
from .lights import *
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import base64
|
||||
|
||||
from .connection import Connection
|
||||
from .request import Request
|
||||
|
||||
|
||||
def capture_get_screens(con: Connection):
|
||||
res = con.request(Request("capture", "get_screens"))
|
||||
return res.get_data()
|
||||
|
||||
|
||||
def capture_get_jpg(con: Connection, screen: int = 0, quality: int = 70, divide: int = 1):
|
||||
req = Request("capture", "get_jpg")
|
||||
req.add_param(screen)
|
||||
req.add_param(quality)
|
||||
req.add_param(divide)
|
||||
data = con.request(req).get_data()
|
||||
|
||||
if len(data) < 4:
|
||||
return None
|
||||
|
||||
return {
|
||||
"timestamp": data[0],
|
||||
"width": data[1],
|
||||
"height": data[2],
|
||||
"data": base64.b64decode(data[3]),
|
||||
}
|
||||
|
||||
|
||||
def capture_get_streams(con: Connection):
|
||||
"""Describes the HTTP video stream, or None when this spice2x serves none."""
|
||||
data = con.request(Request("capture", "get_streams")).get_data()
|
||||
return data[0] if data else None
|
||||
@@ -2,6 +2,10 @@ from .connection import Connection
|
||||
from .request import Request
|
||||
|
||||
|
||||
def card_get_cards(con: Connection):
|
||||
return con.request(Request("card", "get_cards")).get_data()
|
||||
|
||||
|
||||
def card_insert(con: Connection, unit: int, card_id: str):
|
||||
req = Request("card", "insert")
|
||||
req.add_param(unit)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from .connection import Connection
|
||||
from .request import Request
|
||||
|
||||
|
||||
def sdvx_tapeled_get(con: Connection, *light_names):
|
||||
req = Request("sdvx", "tapeled_get")
|
||||
|
||||
for light_name in light_names:
|
||||
req.add_param(light_name)
|
||||
|
||||
res = con.request(req)
|
||||
return res.get_data()
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "controller.h"
|
||||
#include "serial.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
|
||||
@@ -17,7 +18,6 @@ namespace api {
|
||||
controller->init_state(this->state);
|
||||
this->thread = new std::thread([this] () {
|
||||
log_warning("api::serial", "listening on {} (baud: {})", this->port, this->baud);
|
||||
timeutils::PreciseSleepTimer timer;
|
||||
|
||||
// read buffer
|
||||
uint8_t read_buffer[16*1024];
|
||||
@@ -162,7 +162,7 @@ namespace api {
|
||||
|
||||
// slow down on reconnect
|
||||
if (this->running) {
|
||||
timer.sleep(retry_time);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "stream_format.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "h264_stream.h"
|
||||
#include "hooks/graphics/jpeg_encoder.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef SPICE_JPEG
|
||||
constexpr const char *MJPEG_BOUNDARY = "spice2xframe";
|
||||
|
||||
// multipart/x-mixed-replace: every frame is a standalone JPEG, no inter-frame state
|
||||
class MjpegWriter : public StreamWriter {
|
||||
public:
|
||||
|
||||
explicit MjpegWriter(int quality) : quality(quality) {}
|
||||
|
||||
std::string content_type() const override {
|
||||
return std::string("multipart/x-mixed-replace; boundary=") + MJPEG_BOUNDARY;
|
||||
}
|
||||
|
||||
bool write(const StreamSend &send, const capture_pump::Frame &frame) override {
|
||||
this->jpeg.clear();
|
||||
if (!jpeg_encoder::encode(
|
||||
this->jpeg, frame.pixels.get(),
|
||||
frame.width, frame.height, this->quality)) {
|
||||
// a frame the encoder rejects is not worth dropping the client over
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string part =
|
||||
"--" + std::string(MJPEG_BOUNDARY) + "\r\n"
|
||||
"Content-Type: image/jpeg\r\n"
|
||||
"Content-Length: " + std::to_string(this->jpeg.size()) + "\r\n"
|
||||
"\r\n";
|
||||
|
||||
return send(part.data(), part.size())
|
||||
&& send(this->jpeg.data(), this->jpeg.size())
|
||||
&& send("\r\n", 2);
|
||||
}
|
||||
|
||||
private:
|
||||
int quality;
|
||||
std::vector<uint8_t> jpeg;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
// both parameters go unused on toolchains that compile in neither format
|
||||
std::unique_ptr<StreamWriter> make_stream_writer(
|
||||
const std::string &path, [[maybe_unused]] int quality, [[maybe_unused]] int fps) {
|
||||
|
||||
#ifdef SPICE_JPEG
|
||||
if (path == "/stream.mjpg") {
|
||||
return std::make_unique<MjpegWriter>(quality);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SPICE_H264
|
||||
if (path == "/stream.h264") {
|
||||
return make_h264_writer(quality, fps);
|
||||
}
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> stream_formats() {
|
||||
std::vector<std::pair<std::string, std::string>> formats;
|
||||
|
||||
#ifdef SPICE_JPEG
|
||||
formats.emplace_back("mjpeg", "/stream.mjpg");
|
||||
#endif
|
||||
|
||||
#ifdef SPICE_H264
|
||||
formats.emplace_back("h264", "/stream.h264");
|
||||
#endif
|
||||
|
||||
return formats;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "capture_pump.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
// writes bytes to the client; false once the connection is gone
|
||||
using StreamSend = std::function<bool(const void *, size_t)>;
|
||||
|
||||
// one wire format, instantiated per connection so it can keep encoder state across frames
|
||||
class StreamWriter {
|
||||
public:
|
||||
virtual ~StreamWriter() = default;
|
||||
|
||||
StreamWriter(const StreamWriter &) = delete;
|
||||
StreamWriter &operator=(const StreamWriter &) = delete;
|
||||
|
||||
// value for the HTTP Content-Type response header
|
||||
virtual std::string content_type() const = 0;
|
||||
|
||||
// for formats that open with an init segment; runs once before any frame
|
||||
virtual bool begin(const StreamSend &send) { return true; }
|
||||
|
||||
virtual bool write(const StreamSend &send, const capture_pump::Frame &frame) = 0;
|
||||
|
||||
protected:
|
||||
StreamWriter() = default;
|
||||
};
|
||||
|
||||
// null when the path does not name a format this build supports
|
||||
std::unique_ptr<StreamWriter> make_stream_writer(
|
||||
const std::string &path, int quality, int fps);
|
||||
|
||||
// name and path of every format compiled into this build, for clients to pick from
|
||||
std::vector<std::pair<std::string, std::string>> stream_formats();
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include "stream_server.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "capture_pump.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "stream_format.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
namespace {
|
||||
|
||||
struct HttpRequest {
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::map<std::string, std::string> query;
|
||||
};
|
||||
|
||||
bool send_all(SOCKET socket, const void *data, size_t size) {
|
||||
auto cursor = reinterpret_cast<const char *>(data);
|
||||
size_t remaining = size;
|
||||
|
||||
while (remaining > 0) {
|
||||
const int sent = send(socket, cursor, static_cast<int>(remaining), 0);
|
||||
if (sent <= 0) {
|
||||
return false;
|
||||
}
|
||||
cursor += sent;
|
||||
remaining -= static_cast<size_t>(sent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool send_all(SOCKET socket, const std::string &text) {
|
||||
return send_all(socket, text.data(), text.size());
|
||||
}
|
||||
|
||||
// a viewer leaving is normally noticed by a failing send, so a stream with no frame
|
||||
// to push has to ask the socket instead
|
||||
bool client_gone(SOCKET socket) {
|
||||
fd_set read_set;
|
||||
FD_ZERO(&read_set);
|
||||
FD_SET(socket, &read_set);
|
||||
|
||||
// the socket is blocking with a receive timeout, so poll before touching it
|
||||
timeval immediately {};
|
||||
const int ready = select(0, &read_set, nullptr, nullptr, &immediately);
|
||||
if (ready == 0) {
|
||||
return false;
|
||||
}
|
||||
if (ready < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// consumed rather than peeked: a stray byte would otherwise sit in front of the
|
||||
// FIN and keep hiding it for as long as the stream runs
|
||||
char discard[256];
|
||||
return recv(socket, discard, sizeof(discard), 0) <= 0;
|
||||
}
|
||||
|
||||
std::string url_decode(const std::string &input) {
|
||||
std::string out;
|
||||
out.reserve(input.size());
|
||||
|
||||
for (size_t i = 0; i < input.size(); i++) {
|
||||
if (input[i] == '+') {
|
||||
out.push_back(' ');
|
||||
} else if (input[i] == '%' && i + 2 < input.size()
|
||||
&& isxdigit(static_cast<unsigned char>(input[i + 1]))
|
||||
&& isxdigit(static_cast<unsigned char>(input[i + 2]))) {
|
||||
out.push_back(static_cast<char>(
|
||||
std::stoi(input.substr(i + 1, 2), nullptr, 16)));
|
||||
i += 2;
|
||||
} else {
|
||||
out.push_back(input[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void parse_query(const std::string &query, HttpRequest &request) {
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < query.size()) {
|
||||
auto end = query.find('&', pos);
|
||||
if (end == std::string::npos) {
|
||||
end = query.size();
|
||||
}
|
||||
|
||||
const auto pair = query.substr(pos, end - pos);
|
||||
const auto split = pair.find('=');
|
||||
if (split != std::string::npos && split > 0) {
|
||||
request.query[url_decode(pair.substr(0, split))] =
|
||||
url_decode(pair.substr(split + 1));
|
||||
}
|
||||
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// reads the request head only; anything oversized or malformed is refused
|
||||
bool read_request(SOCKET socket, size_t size_limit, HttpRequest &request) {
|
||||
std::string head;
|
||||
char buffer[1024];
|
||||
|
||||
while (head.find("\r\n\r\n") == std::string::npos) {
|
||||
if (head.size() >= size_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// read no further than the limit, so the head cannot overshoot it
|
||||
const size_t budget = std::min(sizeof(buffer), size_limit - head.size());
|
||||
const int received = recv(socket, buffer, static_cast<int>(budget), 0);
|
||||
if (received <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
head.append(buffer, static_cast<size_t>(received));
|
||||
}
|
||||
|
||||
const auto line_end = head.find("\r\n");
|
||||
const auto line = head.substr(0, line_end);
|
||||
|
||||
const auto method_end = line.find(' ');
|
||||
if (method_end == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto target_end = line.find(' ', method_end + 1);
|
||||
if (target_end == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
request.method = line.substr(0, method_end);
|
||||
auto target = line.substr(method_end + 1, target_end - method_end - 1);
|
||||
|
||||
const auto query_start = target.find('?');
|
||||
if (query_start != std::string::npos) {
|
||||
parse_query(target.substr(query_start + 1), request);
|
||||
target = target.substr(0, query_start);
|
||||
}
|
||||
|
||||
request.path = url_decode(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
int query_int(const HttpRequest &request, const std::string &name, int fallback,
|
||||
int min, int max) {
|
||||
|
||||
const auto pos = request.query.find(name);
|
||||
if (pos == request.query.end()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return std::clamp(std::stoi(pos->second), min, max);
|
||||
} catch (const std::exception &) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// an <img> can show a cross-origin stream without this, but a browser client that
|
||||
// decodes the bytes itself has to fetch() them, and fetch is subject to CORS. errors
|
||||
// carry it too, or the client sees an opaque failure instead of the status.
|
||||
constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n";
|
||||
|
||||
// the port is unauthenticated, so a scanner hammering a busy/missing screen could
|
||||
// otherwise flood the overlay; throttle failure toasts per distinct cause. kept under
|
||||
// a second so it only swallows that, not a legitimate reconnect - substream itself
|
||||
// switches screens with a 300ms gap, and only backs off to a full second once a
|
||||
// retry has actually failed
|
||||
constexpr double notification_throttle_seconds = 0.5;
|
||||
|
||||
void send_error(SOCKET socket, const char *status) {
|
||||
const std::string response =
|
||||
std::string("HTTP/1.0 ") + status + "\r\n"
|
||||
+ cors_header +
|
||||
"Content-Length: 0\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
send_all(socket, response);
|
||||
}
|
||||
|
||||
std::atomic<unsigned short> LISTENING_PORT { 0 };
|
||||
}
|
||||
|
||||
unsigned short stream_server_port() {
|
||||
return LISTENING_PORT.load();
|
||||
}
|
||||
|
||||
StreamServer::StreamServer(unsigned short port)
|
||||
: port(port)
|
||||
{
|
||||
// WinXP builds compile in neither encoder, so there would be nothing to serve and
|
||||
// every request would 404; taking the port instead only invites confused clients
|
||||
if (stream_formats().empty()) {
|
||||
log_warning("api::stream",
|
||||
"this build has no video encoders, the video stream is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this->open_listener()) {
|
||||
// the stream was asked for explicitly, so say plainly that it is not there
|
||||
log_warning("api::stream",
|
||||
"the video stream is not available on port {}", this->port);
|
||||
return;
|
||||
}
|
||||
|
||||
this->running = true;
|
||||
this->acceptor = std::thread([this] {
|
||||
this->accept_worker();
|
||||
});
|
||||
|
||||
LISTENING_PORT = this->port;
|
||||
|
||||
// deliberately not logging a full URL; local IPs would leak into shared logs
|
||||
log_info("api::stream", "video stream is listening on port: {}", this->port);
|
||||
log_warning("api::stream",
|
||||
"the video stream is unauthenticated - anyone who can reach port {} can watch "
|
||||
"the game screen", this->port);
|
||||
}
|
||||
|
||||
bool StreamServer::open_listener() {
|
||||
WSADATA wsa_data;
|
||||
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||
if (error != 0) {
|
||||
log_warning("api::stream", "WSAStartup() returned {}", error);
|
||||
return false;
|
||||
}
|
||||
this->wsa_started = true;
|
||||
|
||||
this->listener = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (this->listener == INVALID_SOCKET) {
|
||||
log_warning("api::stream", "could not create listener socket: {}",
|
||||
get_last_error_string());
|
||||
return false;
|
||||
}
|
||||
|
||||
int opt_enable = 1;
|
||||
if (setsockopt(this->listener, SOL_SOCKET, SO_REUSEADDR,
|
||||
reinterpret_cast<const char *>(&opt_enable), sizeof(int)) == -1) {
|
||||
log_warning("api::stream", "could not set socket option SO_REUSEADDR: {}",
|
||||
get_last_error_string());
|
||||
}
|
||||
|
||||
sockaddr_in server_address {};
|
||||
server_address.sin_family = AF_INET;
|
||||
server_address.sin_port = htons(this->port);
|
||||
server_address.sin_addr.s_addr = INADDR_ANY;
|
||||
|
||||
if (bind(this->listener, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
|
||||
log_warning("api::stream", "could not bind socket on port {}: {}",
|
||||
this->port, get_last_error_string());
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(this->listener, server_backlog) == -1) {
|
||||
log_warning("api::stream", "could not listen on port {}: {}",
|
||||
this->port, get_last_error_string());
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
StreamServer::~StreamServer() {
|
||||
|
||||
this->running = false;
|
||||
LISTENING_PORT = 0;
|
||||
|
||||
if (this->listener != INVALID_SOCKET) {
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
// drops the client threads out of their blocking send/recv
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (auto &client : this->clients) {
|
||||
if (client.socket != INVALID_SOCKET) {
|
||||
::shutdown(client.socket, SD_BOTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->acceptor.joinable()) {
|
||||
this->acceptor.join();
|
||||
}
|
||||
|
||||
// joining is what guarantees no client thread outlives this object
|
||||
for (auto &client : this->clients) {
|
||||
if (client.thread.joinable()) {
|
||||
client.thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->wsa_started) {
|
||||
WSACleanup();
|
||||
}
|
||||
}
|
||||
|
||||
void StreamServer::accept_worker() {
|
||||
|
||||
while (this->running) {
|
||||
sockaddr_in client_address {};
|
||||
int client_address_size = sizeof(sockaddr_in);
|
||||
|
||||
const SOCKET client = accept(
|
||||
this->listener, (sockaddr *) &client_address, &client_address_size);
|
||||
if (client == INVALID_SOCKET) {
|
||||
// on shutdown the listener is closed under us; otherwise do not spin
|
||||
if (this->running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this->running) {
|
||||
closesocket(client);
|
||||
break;
|
||||
}
|
||||
|
||||
char address_data[INET_ADDRSTRLEN] {};
|
||||
inet_ntop(AF_INET, &client_address.sin_addr, address_data, INET_ADDRSTRLEN);
|
||||
std::string address(address_data);
|
||||
|
||||
// every client costs an encode and real bandwidth, so the cap protects the game
|
||||
int slot = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (size_t i = 0; i < this->clients.size(); i++) {
|
||||
if (!this->clients[i].active) {
|
||||
this->clients[i].active = true;
|
||||
this->clients[i].socket = client;
|
||||
slot = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (slot < 0) {
|
||||
log_warning("api::stream", "client limit of {} hit", client_limit);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
"api::stream.client_limit",
|
||||
notification_throttle_seconds,
|
||||
fmt::format("Video stream refused: client limit reached ({})", address));
|
||||
send_error(client, "503 Service Unavailable");
|
||||
closesocket(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
// this thread is the only one that touches the thread objects, so the slot's
|
||||
// previous occupant gets reaped here rather than being detached
|
||||
if (this->clients[slot].thread.joinable()) {
|
||||
this->clients[slot].thread.join();
|
||||
}
|
||||
|
||||
this->clients[slot].thread = std::thread([this, slot, client, address] {
|
||||
this->client_worker(slot, client, address);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void StreamServer::client_worker(int slot, SOCKET socket, std::string address) {
|
||||
|
||||
DWORD timeout = request_timeout_ms;
|
||||
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
|
||||
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||
|
||||
timeout = send_timeout_ms;
|
||||
setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO,
|
||||
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||
|
||||
int opt_enable = 1;
|
||||
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
|
||||
reinterpret_cast<const char *>(&opt_enable), sizeof(int));
|
||||
|
||||
// whatever sits in the send buffer is already stale, and the default holds about a
|
||||
// third of a second of H.264 because the bitrate is so low. keeping it small makes a
|
||||
// slow reader block the sender, which then skips to the newest frame instead of
|
||||
// handing over a backlog
|
||||
int send_buffer = send_buffer_bytes;
|
||||
setsockopt(socket, SOL_SOCKET, SO_SNDBUF,
|
||||
reinterpret_cast<const char *>(&send_buffer), sizeof(send_buffer));
|
||||
|
||||
HttpRequest request;
|
||||
if (read_request(socket, request_size_limit, request)) {
|
||||
if (request.method != "GET") {
|
||||
send_error(socket, "405 Method Not Allowed");
|
||||
} else {
|
||||
const int fps = query_int(request, "fps", 30, 1, fps_limit);
|
||||
const int quality = query_int(request, "q", 70, 1, 100);
|
||||
|
||||
auto writer = make_stream_writer(request.path, quality, fps);
|
||||
if (!writer) {
|
||||
send_error(socket, "404 Not Found");
|
||||
} else {
|
||||
std::vector<int> screens;
|
||||
graphics_screens_get(screens);
|
||||
|
||||
// registration takes a raw swapchain index and never bounds it, so the
|
||||
// capture range has to be enforced here rather than assumed
|
||||
const auto streamable = [&screens](int screen) {
|
||||
return screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
|
||||
&& std::find(screens.begin(), screens.end(), screen)
|
||||
!= screens.end();
|
||||
};
|
||||
|
||||
// screen 1 is the subscreen in every game that has one; single-screen games
|
||||
// only ever register screen 0, so resolve the default against what exists.
|
||||
// left unclamped so a nonsense screen is reported as what was asked for
|
||||
int screen = query_int(request, "screen", -1, 0,
|
||||
std::numeric_limits<int>::max());
|
||||
if (screen < 0) {
|
||||
screen = streamable(1) ? 1 : 0;
|
||||
}
|
||||
|
||||
// the default always lands on a screen that exists, so this is only ever
|
||||
// an explicit request for one that cannot be captured
|
||||
if (!streamable(screen)) {
|
||||
log_warning("api::stream",
|
||||
"screen {} is not available, refusing {}", screen, address);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
fmt::format("api::stream.screen_unavailable.{}", screen),
|
||||
notification_throttle_seconds,
|
||||
fmt::format("Video stream refused: screen {} not available ({})",
|
||||
screen, address));
|
||||
send_error(socket, "404 Not Found");
|
||||
} else if (!capture_pump::claim_screen(screen)) {
|
||||
log_warning("api::stream",
|
||||
"screen {} is already being streamed, refusing {}",
|
||||
screen, address);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
fmt::format("api::stream.screen_claimed.{}", screen),
|
||||
notification_throttle_seconds,
|
||||
fmt::format("Video stream refused: screen {} already streaming ({})",
|
||||
screen, address));
|
||||
send_error(socket, "503 Service Unavailable");
|
||||
} else {
|
||||
log_info("api::stream",
|
||||
"client connected: {} ({}, screen={}, fps={}, quality={})",
|
||||
address, request.path, screen, fps, quality);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("Video stream client connected ({}, screen {})",
|
||||
address, screen));
|
||||
|
||||
const std::string header =
|
||||
"HTTP/1.0 200 OK\r\n"
|
||||
"Connection: close\r\n"
|
||||
+ std::string(cors_header) +
|
||||
"Cache-Control: no-store, no-cache, must-revalidate\r\n"
|
||||
"Pragma: no-cache\r\n"
|
||||
"Content-Type: " + writer->content_type() + "\r\n"
|
||||
"\r\n";
|
||||
|
||||
const StreamSend stream_send = [socket](const void *data, size_t size) {
|
||||
return send_all(socket, data, size);
|
||||
};
|
||||
|
||||
if (send_all(socket, header) && writer->begin(stream_send)) {
|
||||
const auto interval = std::chrono::microseconds(1000000 / fps);
|
||||
|
||||
while (this->running) {
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
|
||||
capture_pump::Frame frame;
|
||||
const bool ok = capture_pump::capture_direct(
|
||||
screen, frame.pixels, 1,
|
||||
&frame.timestamp, &frame.width, &frame.height);
|
||||
|
||||
if (ok && frame.pixels) {
|
||||
if (!writer->write(stream_send, frame)) {
|
||||
break;
|
||||
}
|
||||
} else if (client_gone(socket)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// a failed capture still paces, or a stalled game spins this
|
||||
std::this_thread::sleep_until(started + interval);
|
||||
}
|
||||
}
|
||||
|
||||
capture_pump::release_screen(screen);
|
||||
log_info("api::stream", "client disconnected: {}", address);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Info,
|
||||
fmt::format("Video stream client disconnected ({})", address));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
this->clients[slot].socket = INVALID_SOCKET;
|
||||
this->clients[slot].active = false;
|
||||
}
|
||||
|
||||
closesocket(socket);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
namespace api {
|
||||
|
||||
// 0 while no stream server is listening, so the API can tell clients not to look for one
|
||||
unsigned short stream_server_port();
|
||||
|
||||
class StreamServer {
|
||||
public:
|
||||
|
||||
explicit StreamServer(unsigned short port);
|
||||
~StreamServer();
|
||||
|
||||
StreamServer(const StreamServer &) = delete;
|
||||
StreamServer &operator=(const StreamServer &) = delete;
|
||||
|
||||
private:
|
||||
|
||||
// configuration
|
||||
static constexpr int server_backlog = 4;
|
||||
static constexpr int client_limit = 4;
|
||||
static constexpr int request_size_limit = 8 * 1024;
|
||||
static constexpr int request_timeout_ms = 5000;
|
||||
static constexpr int send_timeout_ms = 5000;
|
||||
// small enough that a low bitrate stream cannot hide a backlog of stale frames in it
|
||||
static constexpr int send_buffer_bytes = 16 * 1024;
|
||||
static constexpr int fps_limit = 60;
|
||||
|
||||
struct Client {
|
||||
std::thread thread;
|
||||
SOCKET socket = INVALID_SOCKET;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
void accept_worker();
|
||||
bool open_listener();
|
||||
void client_worker(int slot, SOCKET socket, std::string address);
|
||||
|
||||
unsigned short port;
|
||||
SOCKET listener = INVALID_SOCKET;
|
||||
bool wsa_started = false;
|
||||
std::atomic_bool running { false };
|
||||
std::thread acceptor;
|
||||
std::mutex clients_m;
|
||||
// socket and active are guarded by clients_m; only the acceptor touches thread
|
||||
std::array<Client, client_limit> clients;
|
||||
};
|
||||
}
|
||||
+547
-144
@@ -1,176 +1,579 @@
|
||||
#define HEADSOCKET_IMPLEMENTATION
|
||||
#include "external/headsocket.h"
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include "websocket.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/rc4.h"
|
||||
#include "util/logging.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "controller.h"
|
||||
|
||||
using namespace headsocket;
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "external/wslay/msvc_compat.h"
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
#include "controller.h"
|
||||
#include "external/hash-library/sha1.h"
|
||||
#include "overlay/notifications.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace api {
|
||||
|
||||
/*
|
||||
* Client class declaration
|
||||
*/
|
||||
class WebSocketClient : public web_socket_client {
|
||||
namespace {
|
||||
|
||||
// required class header
|
||||
HEADSOCKET_CLIENT(WebSocketClient, web_socket_client);
|
||||
constexpr int server_backlog = 4;
|
||||
constexpr size_t client_limit = 8;
|
||||
|
||||
private:
|
||||
ClientState *state = nullptr;
|
||||
// a peer that connects and then says nothing must not hold a slot forever
|
||||
constexpr int handshake_timeout_ms = 5000;
|
||||
constexpr size_t request_size_limit = 8 * 1024;
|
||||
constexpr uint64_t message_size_limit = 64 * 1024;
|
||||
|
||||
protected:
|
||||
bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override;
|
||||
// how long a quiet connection waits before the loop rechecks whether we are stopping
|
||||
constexpr int idle_poll_ms = 500;
|
||||
|
||||
void on_accept() override;
|
||||
void on_disconnect() override;
|
||||
};
|
||||
// RFC 6455 appends this to the client key before hashing
|
||||
constexpr const char *websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
/*
|
||||
* Server class declaration
|
||||
*/
|
||||
class WebSocketServer : public web_socket_server<WebSocketClient> {
|
||||
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
|
||||
public:
|
||||
WebSocketController *websocket;
|
||||
};
|
||||
constexpr double notification_throttle_seconds = 0.5;
|
||||
|
||||
void api::WebSocketServer::init() {}
|
||||
std::string trim(const std::string &text) {
|
||||
const auto begin = text.find_first_not_of(" \t");
|
||||
if (begin == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
return text.substr(begin, text.find_last_not_of(" \t") - begin + 1);
|
||||
}
|
||||
|
||||
std::string to_lower(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
bool contains_token(const std::string &value, const std::string &expected) {
|
||||
size_t pos = 0;
|
||||
while (pos < value.size()) {
|
||||
const size_t end = value.find(',', pos);
|
||||
if (to_lower(trim(value.substr(pos, end - pos))) == expected) {
|
||||
return true;
|
||||
}
|
||||
if (end == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
pos = end + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool valid_websocket_key(const std::string &key) {
|
||||
if (key.size() != 24 || key[22] != '=' || key[23] != '=') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string alphabet =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
for (size_t i = 0; i < 22; i++) {
|
||||
if (alphabet.find(key[i]) == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool send_all(SOCKET socket, const std::string &text) {
|
||||
size_t remaining = text.size();
|
||||
const char *cursor = text.data();
|
||||
|
||||
while (remaining > 0) {
|
||||
const int sent = send(socket, cursor, static_cast<int>(remaining), 0);
|
||||
if (sent <= 0) {
|
||||
return false;
|
||||
}
|
||||
cursor += sent;
|
||||
remaining -= static_cast<size_t>(sent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void set_recv_timeout(SOCKET socket, int milliseconds) {
|
||||
const DWORD timeout = static_cast<DWORD>(milliseconds);
|
||||
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
|
||||
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||
}
|
||||
|
||||
std::string accept_key(const std::string &client_key) {
|
||||
SHA1 sha1;
|
||||
sha1.add(client_key.data(), client_key.size());
|
||||
sha1.add(websocket_guid, strlen(websocket_guid));
|
||||
|
||||
unsigned char digest[SHA1::HashBytes] {};
|
||||
sha1.getHash(digest);
|
||||
|
||||
return crypt::base64_encode(digest, sizeof(digest));
|
||||
}
|
||||
|
||||
bool handshake(SOCKET socket) {
|
||||
std::string request;
|
||||
char byte = 0;
|
||||
bool complete = false;
|
||||
|
||||
while (request.size() < request_size_limit) {
|
||||
const int read = recv(socket, &byte, 1, 0);
|
||||
if (read <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
request.push_back(byte);
|
||||
if (request.size() >= 4
|
||||
&& request.compare(request.size() - 4, 4, "\r\n\r\n") == 0) {
|
||||
complete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string key;
|
||||
std::string version;
|
||||
bool connection_upgrade = false;
|
||||
bool upgrade = false;
|
||||
const size_t request_line_end = request.find("\r\n");
|
||||
const std::string request_line = request.substr(0, request_line_end);
|
||||
const size_t target_end = request_line.find(' ', 4);
|
||||
const bool valid_request_line = request_line_end != std::string::npos
|
||||
&& request_line.compare(0, 4, "GET ") == 0
|
||||
&& target_end != std::string::npos
|
||||
&& target_end > 4
|
||||
&& target_end == request_line.rfind(' ')
|
||||
&& request_line.substr(target_end + 1) == "HTTP/1.1";
|
||||
size_t pos = request_line_end;
|
||||
|
||||
while (pos != std::string::npos) {
|
||||
const size_t end = request.find("\r\n", pos + 2);
|
||||
if (end == std::string::npos || end == pos + 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
const std::string line = request.substr(pos + 2, end - pos - 2);
|
||||
const size_t colon = line.find(':');
|
||||
if (colon != std::string::npos) {
|
||||
const std::string name = to_lower(trim(line.substr(0, colon)));
|
||||
const std::string value = trim(line.substr(colon + 1));
|
||||
|
||||
if (name == "sec-websocket-key") {
|
||||
key = value;
|
||||
} else if (name == "sec-websocket-version") {
|
||||
version = value;
|
||||
} else if (name == "connection") {
|
||||
connection_upgrade = connection_upgrade
|
||||
|| contains_token(value, "upgrade");
|
||||
} else if (name == "upgrade") {
|
||||
upgrade = upgrade || contains_token(value, "websocket");
|
||||
}
|
||||
}
|
||||
|
||||
pos = end;
|
||||
}
|
||||
|
||||
if (!complete || !valid_request_line || !connection_upgrade || !upgrade
|
||||
|| version != "13" || !valid_websocket_key(key)) {
|
||||
send_all(socket,
|
||||
"HTTP/1.1 400 Bad Request\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Length: 0\r\n"
|
||||
"\r\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// deliberately echoes back no extension or subprotocol: naming one the client did
|
||||
// not offer is a handshake failure, and none of them are wanted here
|
||||
return send_all(socket,
|
||||
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Accept: " + accept_key(key) + "\r\n"
|
||||
"\r\n");
|
||||
}
|
||||
|
||||
// everything one connection needs; wslay hands this back to the callbacks
|
||||
struct Session {
|
||||
SOCKET socket = INVALID_SOCKET;
|
||||
Controller *controller = nullptr;
|
||||
ClientState *state = nullptr;
|
||||
bool failed = false;
|
||||
};
|
||||
|
||||
ssize_t recv_callback(wslay_event_context_ptr ctx, uint8_t *buffer, size_t length,
|
||||
int flags, void *user_data) {
|
||||
|
||||
(void) flags;
|
||||
|
||||
auto *session = static_cast<Session *>(user_data);
|
||||
const int read = recv(session->socket, reinterpret_cast<char *>(buffer),
|
||||
static_cast<int>(length), 0);
|
||||
|
||||
if (read > 0) {
|
||||
return read;
|
||||
}
|
||||
|
||||
// the socket is non-blocking, so an empty one has to read as "nothing yet"
|
||||
// rather than as a dead peer
|
||||
if (read < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||
return -1;
|
||||
}
|
||||
|
||||
wslay_event_set_error(ctx,
|
||||
read == 0 ? WSLAY_ERR_NO_MORE_MSG : WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t length,
|
||||
int flags, void *user_data) {
|
||||
|
||||
(void) flags;
|
||||
|
||||
auto *session = static_cast<Session *>(user_data);
|
||||
const int sent = send(session->socket, reinterpret_cast<const char *>(data),
|
||||
static_cast<int>(length), 0);
|
||||
|
||||
if (sent > 0) {
|
||||
return sent;
|
||||
}
|
||||
|
||||
if (sent < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||
return -1;
|
||||
}
|
||||
|
||||
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void on_msg_recv(wslay_event_context_ptr ctx,
|
||||
const struct wslay_event_on_msg_recv_arg *arg, void *user_data) {
|
||||
|
||||
auto *session = static_cast<Session *>(user_data);
|
||||
|
||||
// pings and closes are wslay's business, it answers them itself
|
||||
if (wslay_is_ctrl_frame(arg->opcode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg->opcode != WSLAY_BINARY_FRAME) {
|
||||
log_warning("api::websocket", "ignoring a non-binary message");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<char> in(arg->msg, arg->msg + arg->msg_length);
|
||||
std::vector<char> out;
|
||||
|
||||
if (session->state->cipher) {
|
||||
session->state->cipher->crypt(
|
||||
reinterpret_cast<uint8_t *>(in.data()), in.size());
|
||||
}
|
||||
|
||||
session->controller->process_request(session->state, &in, &out);
|
||||
|
||||
if (session->state->cipher) {
|
||||
session->state->cipher->crypt(
|
||||
reinterpret_cast<uint8_t *>(out.data()), out.size());
|
||||
}
|
||||
|
||||
wslay_event_msg reply {};
|
||||
reply.opcode = WSLAY_BINARY_FRAME;
|
||||
reply.msg = reinterpret_cast<const uint8_t *>(out.data());
|
||||
reply.msg_length = out.size();
|
||||
|
||||
if (wslay_event_queue_msg(ctx, &reply) != 0) {
|
||||
session->failed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Controller::process_password_change(session->state);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Controller state so we don't have to import headsocket stuff in our header
|
||||
*/
|
||||
struct WebSocketControllerState {
|
||||
std::shared_ptr<WebSocketServer> server;
|
||||
struct Client {
|
||||
std::thread thread;
|
||||
SOCKET socket = INVALID_SOCKET;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
Controller *controller = nullptr;
|
||||
unsigned short port = 0;
|
||||
SOCKET listener = INVALID_SOCKET;
|
||||
bool wsa_started = false;
|
||||
std::atomic_bool running { false };
|
||||
std::thread acceptor;
|
||||
std::mutex clients_m;
|
||||
std::array<Client, client_limit> clients;
|
||||
|
||||
bool open_listener();
|
||||
void accept_worker();
|
||||
void client_worker(int slot, SOCKET socket, std::string address);
|
||||
void stop();
|
||||
};
|
||||
|
||||
bool WebSocketControllerState::open_listener() {
|
||||
WSADATA wsa_data;
|
||||
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||
if (error != 0) {
|
||||
log_warning("api::websocket", "WSAStartup() returned {}", error);
|
||||
return false;
|
||||
}
|
||||
this->wsa_started = true;
|
||||
|
||||
this->listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (this->listener == INVALID_SOCKET) {
|
||||
log_warning("api::websocket", "socket() returned {}", WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_in address {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(this->port);
|
||||
|
||||
if (bind(this->listener, reinterpret_cast<sockaddr *>(&address), sizeof(address))
|
||||
== SOCKET_ERROR) {
|
||||
log_warning("api::websocket", "bind() returned {}", WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(this->listener, server_backlog) == SOCKET_ERROR) {
|
||||
log_warning("api::websocket", "listen() returned {}", WSAGetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebSocketControllerState::accept_worker() {
|
||||
|
||||
while (this->running) {
|
||||
sockaddr_in client_address {};
|
||||
int client_address_size = sizeof(sockaddr_in);
|
||||
|
||||
const SOCKET client = accept(
|
||||
this->listener, reinterpret_cast<sockaddr *>(&client_address),
|
||||
&client_address_size);
|
||||
|
||||
if (client == INVALID_SOCKET) {
|
||||
// on shutdown the listener is closed under us; otherwise do not spin
|
||||
if (this->running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this->running) {
|
||||
closesocket(client);
|
||||
break;
|
||||
}
|
||||
|
||||
// formatted by hand rather than with inet_ntop, which needs a newer Windows than
|
||||
// the XP toolchain targets, or inet_ntoa, which answers from a shared buffer
|
||||
const uint32_t raw = ntohl(client_address.sin_addr.s_addr);
|
||||
const std::string address = fmt::format("{}.{}.{}.{}",
|
||||
(raw >> 24) & 0xff, (raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff);
|
||||
|
||||
int slot = -1;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (size_t i = 0; i < this->clients.size(); i++) {
|
||||
if (!this->clients[i].active) {
|
||||
this->clients[i].active = true;
|
||||
this->clients[i].socket = client;
|
||||
slot = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (slot < 0) {
|
||||
log_warning("api::websocket", "client limit of {} hit", client_limit);
|
||||
overlay::notifications::add_throttled(
|
||||
overlay::notifications::Severity::Warning,
|
||||
"api::websocket.client_limit",
|
||||
notification_throttle_seconds,
|
||||
fmt::format("API websocket refused: client limit reached ({})", address));
|
||||
closesocket(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
// this thread is the only one that touches the thread objects, so the slot's
|
||||
// previous occupant gets reaped here rather than being detached
|
||||
if (this->clients[slot].thread.joinable()) {
|
||||
this->clients[slot].thread.join();
|
||||
}
|
||||
|
||||
// the handshake runs on the client thread on purpose: doing it here would put
|
||||
// every later connection behind whatever this one is waiting for
|
||||
this->clients[slot].thread = std::thread([this, slot, client, address] {
|
||||
this->client_worker(slot, client, address);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketControllerState::client_worker(int slot, SOCKET socket, std::string address) {
|
||||
|
||||
// wslay writes a frame header and its payload as separate sends, so leaving Nagle on
|
||||
// holds the payload back until the peer acknowledges the header, costing a delayed
|
||||
// ack per message; requests here are small and latency sensitive
|
||||
int nodelay = 1;
|
||||
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
|
||||
reinterpret_cast<const char *>(&nodelay), sizeof(nodelay));
|
||||
|
||||
set_recv_timeout(socket, handshake_timeout_ms);
|
||||
|
||||
if (handshake(socket)) {
|
||||
// wslay reads until the socket would block, so leaving it blocking would make
|
||||
// every reply wait out a receive timeout before the send got a turn
|
||||
u_long non_blocking = 1;
|
||||
ioctlsocket(socket, FIONBIO, &non_blocking);
|
||||
|
||||
Session session;
|
||||
session.socket = socket;
|
||||
session.controller = this->controller;
|
||||
session.state = new ClientState();
|
||||
this->controller->init_state(session.state);
|
||||
|
||||
log_info("api::websocket", "client connected: {}", address);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
fmt::format("API websocket client connected ({})", address));
|
||||
|
||||
wslay_event_callbacks callbacks {};
|
||||
callbacks.recv_callback = recv_callback;
|
||||
callbacks.send_callback = send_callback;
|
||||
callbacks.on_msg_recv_callback = on_msg_recv;
|
||||
|
||||
wslay_event_context_ptr ctx = nullptr;
|
||||
if (wslay_event_context_server_init(&ctx, &callbacks, &session) == 0) {
|
||||
wslay_event_config_set_max_recv_msg_length(ctx, message_size_limit);
|
||||
while (this->running && !session.failed
|
||||
&& (wslay_event_want_read(ctx) || wslay_event_want_write(ctx))) {
|
||||
|
||||
fd_set read_set;
|
||||
fd_set write_set;
|
||||
FD_ZERO(&read_set);
|
||||
FD_ZERO(&write_set);
|
||||
|
||||
if (wslay_event_want_read(ctx)) {
|
||||
FD_SET(socket, &read_set);
|
||||
}
|
||||
if (wslay_event_want_write(ctx)) {
|
||||
FD_SET(socket, &write_set);
|
||||
}
|
||||
|
||||
// bounded so a silent connection still notices us shutting down
|
||||
timeval timeout {};
|
||||
timeout.tv_usec = idle_poll_ms * 1000;
|
||||
|
||||
if (select(0, &read_set, &write_set, nullptr, &timeout) < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (FD_ISSET(socket, &read_set) && wslay_event_recv(ctx) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// unconditional, so a reply queued by the read above goes out now
|
||||
if (wslay_event_send(ctx) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wslay_event_context_free(ctx);
|
||||
}
|
||||
|
||||
Controller::free_state(session.state);
|
||||
delete session.state;
|
||||
|
||||
log_info("api::websocket", "client disconnected: {}", address);
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Info,
|
||||
fmt::format("API websocket client disconnected ({})", address));
|
||||
}
|
||||
|
||||
closesocket(socket);
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
this->clients[slot].socket = INVALID_SOCKET;
|
||||
this->clients[slot].active = false;
|
||||
}
|
||||
|
||||
void WebSocketControllerState::stop() {
|
||||
this->running = false;
|
||||
|
||||
if (this->listener != INVALID_SOCKET) {
|
||||
closesocket(this->listener);
|
||||
this->listener = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
// drops the client threads out of their blocking send/recv
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||
for (auto &client : this->clients) {
|
||||
if (client.socket != INVALID_SOCKET) {
|
||||
::shutdown(client.socket, SD_BOTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->acceptor.joinable()) {
|
||||
this->acceptor.join();
|
||||
}
|
||||
|
||||
// joining is what guarantees no client thread outlives this object
|
||||
for (auto &client : this->clients) {
|
||||
if (client.thread.joinable()) {
|
||||
client.thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->wsa_started) {
|
||||
WSACleanup();
|
||||
this->wsa_started = false;
|
||||
}
|
||||
}
|
||||
|
||||
WebSocketController::WebSocketController(Controller *controller, uint16_t port) {
|
||||
this->controller = controller;
|
||||
|
||||
// create state
|
||||
this->state = new WebSocketControllerState();
|
||||
this->state->controller = controller;
|
||||
this->state->port = port;
|
||||
|
||||
// start server
|
||||
this->state->server = WebSocketServer::create(port);
|
||||
this->state->server->websocket = this;
|
||||
if (this->state->server->is_running()) {
|
||||
log_info("api::websocket", "server listening on port: {}", port);
|
||||
} else {
|
||||
if (!this->state->open_listener()) {
|
||||
log_warning("api::websocket", "server failed to listen on port: {}", port);
|
||||
return;
|
||||
}
|
||||
|
||||
this->state->running = true;
|
||||
this->state->acceptor = std::thread([this] {
|
||||
this->state->accept_worker();
|
||||
});
|
||||
|
||||
log_info("api::websocket", "server listening on port: {}", port);
|
||||
}
|
||||
|
||||
WebSocketController::~WebSocketController() {
|
||||
|
||||
// stop server
|
||||
this->state->server->stop();
|
||||
|
||||
// delete state
|
||||
this->state->stop();
|
||||
delete this->state;
|
||||
this->state = nullptr;
|
||||
}
|
||||
|
||||
void WebSocketController::free_socket() {
|
||||
this->state->server->stop();
|
||||
}
|
||||
|
||||
void WebSocketClient::on_accept() {
|
||||
web_socket_client::on_accept();
|
||||
|
||||
// get pointer to server
|
||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||
if (!srv || !srv->websocket) {
|
||||
log_fatal("api::websocket", "on_accept has no server");
|
||||
}
|
||||
|
||||
// check for init
|
||||
state = new ClientState();
|
||||
srv->websocket->controller->init_state(state);
|
||||
|
||||
// log connection
|
||||
log_info("api::websocket", "client connected");
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Success,
|
||||
"API websocket client connected");
|
||||
}
|
||||
|
||||
void WebSocketClient::on_disconnect() {
|
||||
|
||||
// log disconnection
|
||||
log_info("api::websocket", "client disconnected");
|
||||
overlay::notifications::add(
|
||||
overlay::notifications::Severity::Info,
|
||||
"API websocket client disconnected");
|
||||
|
||||
// get pointer to server
|
||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||
if (!srv || !srv->websocket) {
|
||||
log_fatal("api::websocket", "on_disconnect has no server");
|
||||
}
|
||||
|
||||
// clean up state
|
||||
srv->websocket->controller->free_state(state);
|
||||
delete state;
|
||||
state = nullptr;
|
||||
|
||||
// call super
|
||||
web_socket_client::on_disconnect();
|
||||
}
|
||||
|
||||
/*
|
||||
* This is where business actually happens, gets called on every datablock receive
|
||||
*/
|
||||
bool WebSocketClient::async_received_data(const data_block &db, uint8_t *ptr, size_t length) {
|
||||
|
||||
// get pointer to server
|
||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
||||
if (!srv || !srv->websocket) {
|
||||
log_fatal("api::websocket", "received datablock without server");
|
||||
}
|
||||
|
||||
// check state
|
||||
if (!state) {
|
||||
log_fatal("api::websocket", "client with no state received datablock");
|
||||
}
|
||||
|
||||
// check datablock type
|
||||
switch (db.op) {
|
||||
case opcode::binary: {
|
||||
|
||||
// allocate buffers
|
||||
std::vector<char> in(ptr, ptr + length);
|
||||
std::vector<char> out;
|
||||
|
||||
// crypt in-data
|
||||
if (state->cipher) {
|
||||
state->cipher->crypt(reinterpret_cast<uint8_t *>(in.data()), in.size());
|
||||
}
|
||||
|
||||
// process request
|
||||
srv->websocket->controller->process_request(state, &in, &out);
|
||||
|
||||
// crypt out-data
|
||||
if (state->cipher) {
|
||||
state->cipher->crypt(reinterpret_cast<uint8_t *>(out.data()), out.size());
|
||||
}
|
||||
|
||||
// send answer
|
||||
push(out.data(), out.size());
|
||||
|
||||
// check for password change
|
||||
srv->websocket->controller->process_password_change(state);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
log_warning("api::websocket", "datablock received with non-binary type");
|
||||
break;
|
||||
}
|
||||
|
||||
// always consume the datablock, nomnom
|
||||
return true;
|
||||
this->state->stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1114,6 +1114,7 @@ namespace avs {
|
||||
#endif
|
||||
// jubeat
|
||||
{"jubeat.dll", 0x2000000},
|
||||
{"jubeat2019.dll", 0x10000000},
|
||||
|
||||
// MUSECA
|
||||
{"museca.dll", 0xC000000},
|
||||
@@ -1924,7 +1925,7 @@ namespace avs {
|
||||
#if !SPICE64
|
||||
// sdvx4 bad log config fix
|
||||
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
|
||||
property_search_safe(config, config_node, "/log/enable_console")) {
|
||||
property_search(config, config_node, "/log/enable_console")) {
|
||||
log_info("avs-core", "applying SDVX4 avs-config.xml fix for <log><enable_console>");
|
||||
property_search_remove_safe(config, config_node, "/log/enable_console");
|
||||
}
|
||||
|
||||
@@ -591,6 +591,7 @@ namespace avs {
|
||||
// for proper reporting of Omnimix and other song packs
|
||||
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
|
||||
_stricmp(EA3_MODEL, "L44") == 0 ||
|
||||
_stricmp(EA3_MODEL, "T44") == 0 ||
|
||||
_stricmp(EA3_MODEL, "M39") == 0 ||
|
||||
_stricmp(EA3_MODEL, "KFC") == 0)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<dpiAware>true</dpiAware>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<dependency>
|
||||
|
||||
+66
-11
@@ -3,7 +3,9 @@
|
||||
#include <cassert>
|
||||
#include <optional>
|
||||
|
||||
#include "games/io.h"
|
||||
#include "launcher/superexit.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "rawinput/rawinput.h"
|
||||
#include "rawinput/piuio.h"
|
||||
#include "util/time.h"
|
||||
@@ -62,7 +64,43 @@ std::vector<Button> GameAPI::Buttons::sortButtons(
|
||||
return sorted;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *manager, Button &_button, bool check_alts) {
|
||||
namespace GameAPI::Buttons {
|
||||
static State get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &button,
|
||||
bool check_alts,
|
||||
bool check_modifiers);
|
||||
|
||||
static bool modifiers_pressed(rawinput::RawInputManager *manager, Button &button);
|
||||
}
|
||||
|
||||
bool GameAPI::Buttons::modifiers_pressed(rawinput::RawInputManager *manager, Button &button) {
|
||||
const auto modifier_mask = button.getModifierMask();
|
||||
if (modifier_mask == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto *modifier_buttons = games::get_buttons_modifiers(eamuse_get_game());
|
||||
if (!modifier_buttons) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t index = 0; index < games::ModifierButtons::Size; index++) {
|
||||
if ((modifier_mask & (UINT8_C(1) << index)) != 0 &&
|
||||
(index >= modifier_buttons->size() ||
|
||||
get_button_state(manager, modifier_buttons->at(index), true, false) !=
|
||||
GameAPI::Buttons::BUTTON_PRESSED)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
GameAPI::Buttons::State GameAPI::Buttons::get_button_state(
|
||||
rawinput::RawInputManager *manager,
|
||||
Button &_button,
|
||||
bool check_alts,
|
||||
bool check_modifiers) {
|
||||
|
||||
// check override
|
||||
if (_button.override_enabled) {
|
||||
@@ -76,6 +114,23 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
std::optional<bool> window_has_focus;
|
||||
while (true) {
|
||||
|
||||
// skip bindings whose required modifiers are not held
|
||||
//
|
||||
// note that modifiers cannot process MIDI as the logic is written
|
||||
// below, since MIDI buttons are event-based (on event, off event, etc)
|
||||
// and cannot be correctly handled by a simple early return
|
||||
// there is no explicit check for MIDI here, but the UI should have
|
||||
// prevented it
|
||||
if (check_modifiers && !modifiers_pressed(manager, *current_button)) {
|
||||
button_count++;
|
||||
if (!alternatives || alternatives->empty() ||
|
||||
button_count - 1 >= alternatives->size()) {
|
||||
return BUTTON_NOT_PRESSED;
|
||||
}
|
||||
current_button = &alternatives->at(button_count - 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (current_button->isNaive()) {
|
||||
GameAPI::Buttons::State state;
|
||||
@@ -469,6 +524,10 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
}
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(rawinput::RawInputManager *manager, Button &button, bool check_alts) {
|
||||
return get_button_state(manager, button, check_alts, true);
|
||||
}
|
||||
|
||||
Buttons::State Buttons::getState(std::unique_ptr<rawinput::RawInputManager> &manager, Button &button, bool check_alts) {
|
||||
if (manager) {
|
||||
return getState(manager.get(), button, check_alts);
|
||||
@@ -484,17 +543,13 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
|
||||
return button.override_velocity;
|
||||
}
|
||||
|
||||
// naive behavior
|
||||
if (button.isNaive()) {
|
||||
if (button.getInvert()) {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 0.f : 1.f;
|
||||
} else {
|
||||
return (GetAsyncKeyState(button.getVKey()) & 0x8000) ? 1.f : 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// get button state
|
||||
Buttons::State button_state = Buttons::getState(manager, button, false);
|
||||
const auto button_state = Buttons::getState(manager, button, false);
|
||||
|
||||
// naive bindings report their digital state as full or zero velocity
|
||||
if (button.isNaive()) {
|
||||
return button_state == Buttons::BUTTON_PRESSED ? 1.f : 0.f;
|
||||
}
|
||||
|
||||
// check if button isn't being pressed
|
||||
if (button_state != Buttons::BUTTON_PRESSED) {
|
||||
|
||||
@@ -299,7 +299,27 @@ std::string Button::getMidiNoteString() {
|
||||
return fmt::format("{}{}", note_names[index % 12], ((index / 12) - 1));
|
||||
}
|
||||
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager* manager) {
|
||||
std::string Button::getDisplayString(rawinput::RawInputManager *manager) {
|
||||
auto binding_display = this->getBindingDisplayString(manager);
|
||||
auto modifiers = this->getModifierMask();
|
||||
if (binding_display.empty() || modifiers == 0) {
|
||||
return binding_display;
|
||||
}
|
||||
|
||||
std::string modifier_display;
|
||||
for (unsigned int index = 1; modifiers != 0; index++, modifiers >>= 1) {
|
||||
if ((modifiers & UINT8_C(1)) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!modifier_display.empty()) {
|
||||
modifier_display += "+";
|
||||
}
|
||||
modifier_display += fmt::format("Mod{}", index);
|
||||
}
|
||||
return modifier_display + "+" + binding_display;
|
||||
}
|
||||
|
||||
std::string Button::getBindingDisplayString(rawinput::RawInputManager *manager) {
|
||||
|
||||
// get VKey string
|
||||
auto vKey = (uint16_t) this->getVKey();
|
||||
|
||||
@@ -49,6 +49,7 @@ private:
|
||||
std::string name;
|
||||
std::string device_identifier = "";
|
||||
unsigned short vKey = INVALID_VKEY;
|
||||
uint8_t modifier_mask = 0;
|
||||
|
||||
// default bindings are always naive
|
||||
unsigned short vKey_default = INVALID_VKEY;
|
||||
@@ -65,6 +66,7 @@ private:
|
||||
unsigned short velocity_threshold = 0;
|
||||
|
||||
std::string getMidiNoteString();
|
||||
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
|
||||
|
||||
public:
|
||||
std::string getVKeyString();
|
||||
@@ -111,6 +113,7 @@ public:
|
||||
device_identifier = "";
|
||||
analog_type = BAT_NONE;
|
||||
bat_threshold = 0;
|
||||
modifier_mask = 0;
|
||||
}
|
||||
|
||||
std::string getDisplayString(rawinput::RawInputManager* manager);
|
||||
@@ -123,6 +126,14 @@ public:
|
||||
return this->name;
|
||||
}
|
||||
|
||||
inline uint8_t getModifierMask() const {
|
||||
return this->modifier_mask;
|
||||
}
|
||||
|
||||
inline void setModifierMask(uint8_t new_modifier_mask) {
|
||||
this->modifier_mask = new_modifier_mask & UINT8_C(0x0F);
|
||||
}
|
||||
|
||||
inline const std::string &getDeviceIdentifier() const {
|
||||
return this->device_identifier;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
tinyxml2::XMLError attrError = gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
if (attrError != tinyxml2::XMLError::XML_SUCCESS) {
|
||||
gameButtonsNode->DeleteChild(gameButtonNode);
|
||||
gameButtonNode = this->configFile.NewElement("button");
|
||||
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", bat_threshold);
|
||||
gameButtonNode->SetAttribute("velocity_threshold", velocity_threshold);
|
||||
gameButtonNode->SetAttribute("invert", invert);
|
||||
gameButtonNode->SetAttribute("modifiers", button->getModifierMask());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
} else {
|
||||
button->setVKey(static_cast<unsigned short int>(vKey));
|
||||
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
button->setBatThreshold(bat_threshold);
|
||||
button->setVelocityThreshold(velocity_threshold);
|
||||
button->setInvert(invert);
|
||||
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button->setDeviceIdentifier(devid);
|
||||
}
|
||||
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
|
||||
gameButtonNode->SetAttribute("bat_threshold", it.getBatThreshold());
|
||||
gameButtonNode->SetAttribute("velocity_threshold", it.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", it.getInvert());
|
||||
gameButtonNode->SetAttribute("modifiers", it.getModifierMask());
|
||||
gameButtonNode->SetAttribute("devid", it.getDeviceIdentifier().c_str());
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
|
||||
gameButtonNode->SetAttribute("invert", button.getInvert());
|
||||
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
|
||||
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
|
||||
gameButtonNode->SetAttribute("velocity_threshold", 0);
|
||||
gameButtonNode->SetAttribute("invert", false);
|
||||
gameButtonNode->SetAttribute("devid", "");
|
||||
gameButtonNode->SetAttribute("modifiers", 0);
|
||||
gameButtonsNode->InsertEndChild(gameButtonNode);
|
||||
}
|
||||
}
|
||||
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
int bat_threshold = 0;
|
||||
int velocity_threshold = 0;
|
||||
bool invert = false;
|
||||
unsigned int modifier_mask = 0;
|
||||
gameButtonNode->QueryIntAttribute("vkey", &vKey);
|
||||
gameButtonNode->QueryIntAttribute("analogtype", &analogType);
|
||||
gameButtonNode->QueryDoubleAttribute("debounce_up", &debounce_up);
|
||||
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
gameButtonNode->QueryIntAttribute("bat_threshold", &bat_threshold);
|
||||
gameButtonNode->QueryIntAttribute("velocity_threshold", &velocity_threshold);
|
||||
gameButtonNode->QueryBoolAttribute("invert", &invert);
|
||||
gameButtonNode->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
const char *devid = gameButtonNode->Attribute("devid");
|
||||
|
||||
// find alternative
|
||||
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
alt.setBatThreshold(bat_threshold);
|
||||
alt.setVelocityThreshold(velocity_threshold);
|
||||
alt.setInvert(invert);
|
||||
alt.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
alt.setDeviceIdentifier(std::string(devid));
|
||||
}
|
||||
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
|
||||
button.setBatThreshold(bat_threshold);
|
||||
button.setVelocityThreshold(velocity_threshold);
|
||||
button.setInvert(invert);
|
||||
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
|
||||
if (devid) {
|
||||
button.setDeviceIdentifier(devid);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace overlay::windows {
|
||||
el->SetAttribute("debounce_down", entry.debounce_down);
|
||||
el->SetAttribute("bat_threshold", entry.bat_threshold);
|
||||
el->SetAttribute("velocity_threshold", entry.velocity_threshold);
|
||||
el->SetAttribute("modifiers", entry.modifier_mask);
|
||||
parent->InsertEndChild(el);
|
||||
}
|
||||
|
||||
@@ -54,6 +55,10 @@ namespace overlay::windows {
|
||||
el->QueryIntAttribute("bat_threshold", &bat);
|
||||
entry.bat_threshold = bat;
|
||||
|
||||
unsigned int modifier_mask = 0;
|
||||
el->QueryUnsignedAttribute("modifiers", &modifier_mask);
|
||||
entry.modifier_mask = static_cast<uint8_t>(modifier_mask & UINT8_C(0x0F));
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -160,6 +165,28 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
|
||||
// modifier buttons
|
||||
auto *mod_el = tmpl_el->FirstChildElement("modifier_buttons");
|
||||
if (mod_el) {
|
||||
auto *btn_el = mod_el->FirstChildElement("button");
|
||||
while (btn_el) {
|
||||
const char *btn_name = btn_el->Attribute("name");
|
||||
std::string btn_name_str = btn_name ? btn_name : "";
|
||||
|
||||
TemplateButtonBinding *binding = nullptr;
|
||||
for (auto &b : tmpl.modifier_buttons) {
|
||||
if (b.name == btn_name_str) { binding = &b; break; }
|
||||
}
|
||||
if (!binding) {
|
||||
tmpl.modifier_buttons.push_back(
|
||||
{btn_name_str, read_button_entry(btn_el), {}});
|
||||
} else {
|
||||
binding->alternatives.push_back(read_button_entry(btn_el));
|
||||
}
|
||||
btn_el = btn_el->NextSiblingElement("button");
|
||||
}
|
||||
}
|
||||
|
||||
// keypad buttons
|
||||
auto *kp_el = tmpl_el->FirstChildElement("keypad_buttons");
|
||||
if (kp_el) {
|
||||
@@ -244,6 +271,7 @@ namespace overlay::windows {
|
||||
bool save_user_template(
|
||||
const ControllerTemplate &tmpl,
|
||||
const bool save_buttons,
|
||||
const bool save_modifiers,
|
||||
const bool save_keypads,
|
||||
const bool save_analogs,
|
||||
const bool save_lights) {
|
||||
@@ -300,6 +328,24 @@ namespace overlay::windows {
|
||||
}
|
||||
tmpl_el->InsertEndChild(buttons_el);
|
||||
|
||||
// modifier buttons - skip unbound entries
|
||||
if (!tmpl.modifier_buttons.empty()) {
|
||||
auto *mod_el = doc.NewElement("modifier_buttons");
|
||||
if (save_modifiers) {
|
||||
for (auto &btn : tmpl.modifier_buttons) {
|
||||
if (!btn.primary.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, btn.primary);
|
||||
}
|
||||
for (auto &alt : btn.alternatives) {
|
||||
if (!alt.is_unbound()) {
|
||||
write_button_entry(doc, mod_el, btn.name, alt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tmpl_el->InsertEndChild(mod_el);
|
||||
}
|
||||
|
||||
// keypad buttons — skip unbound entries
|
||||
if (!tmpl.keypad_buttons.empty()) {
|
||||
auto *kp_el = doc.NewElement("keypad_buttons");
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
# fails the build if a PE binary statically imports a forbidden DLL.
|
||||
#
|
||||
# some DLLs must never end up in spice's static import table, for two reasons:
|
||||
#
|
||||
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
|
||||
# into the modules directory to replace the system one. a static import
|
||||
# forces the loader to load the SYSTEM copy at process startup - before the
|
||||
# modules directory is added to the DLL search path and before the game DLL
|
||||
# loads - so the user-supplied override never takes effect (see issue #779).
|
||||
#
|
||||
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
|
||||
# mfreadwrite): a static import loads them eagerly and breaks Unity games.
|
||||
#
|
||||
# in both cases the DLL must instead be loaded dynamically (libutils::try_library
|
||||
# / GetProcAddress / delay load) so it is only pulled in when actually needed.
|
||||
#
|
||||
# invoked via `cmake -P` from a POST_BUILD step. required -D variables:
|
||||
# OBJDUMP - path to objdump (CMAKE_OBJDUMP)
|
||||
# TARGET_FILE - path to the PE binary to inspect
|
||||
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
|
||||
|
||||
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
|
||||
message(WARNING
|
||||
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
|
||||
OUTPUT_VARIABLE dump_output
|
||||
RESULT_VARIABLE dump_result
|
||||
ERROR_VARIABLE dump_error)
|
||||
|
||||
if(NOT dump_result EQUAL 0)
|
||||
message(WARNING
|
||||
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
|
||||
# statically imported DLL in their PE private-header dump.
|
||||
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
|
||||
|
||||
set(violations "")
|
||||
foreach(line IN LISTS dll_lines)
|
||||
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
|
||||
string(STRIP "${dll_name}" dll_name)
|
||||
string(TOLOWER "${dll_name}" dll_name_lower)
|
||||
if(dll_name_lower IN_LIST FORBIDDEN)
|
||||
list(APPEND violations "${dll_name}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(violations)
|
||||
list(REMOVE_DUPLICATES violations)
|
||||
string(REPLACE ";" ", " violations_str "${violations}")
|
||||
message(FATAL_ERROR
|
||||
"static DLL import check FAILED for ${TARGET_FILE}\n"
|
||||
" forbidden static imports found: ${violations_str}\n"
|
||||
"\n"
|
||||
" these DLLs must never be statically imported by spice:\n"
|
||||
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
|
||||
" system copy at startup and preempts the modules override (issue #779).\n"
|
||||
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
|
||||
" Unity games.\n"
|
||||
"\n"
|
||||
" fix: load the DLL dynamically instead - replace the direct API call with a\n"
|
||||
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
|
||||
" call through the resolved function pointer.")
|
||||
endif()
|
||||
|
||||
message(STATUS "static DLL import check passed for ${TARGET_FILE}")
|
||||
# fails the build if a PE binary statically imports a forbidden DLL.
|
||||
#
|
||||
# some DLLs must never end up in spice's static import table, for two reasons:
|
||||
#
|
||||
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
|
||||
# into the modules directory to replace the system one. a static import
|
||||
# forces the loader to load the SYSTEM copy at process startup - before the
|
||||
# modules directory is added to the DLL search path and before the game DLL
|
||||
# loads - so the user-supplied override never takes effect (see issue #779).
|
||||
#
|
||||
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
|
||||
# mfreadwrite): a static import loads them eagerly and breaks Unity games.
|
||||
#
|
||||
# in both cases the DLL must instead be loaded dynamically (libutils::try_library
|
||||
# / GetProcAddress / delay load) so it is only pulled in when actually needed.
|
||||
#
|
||||
# invoked via `cmake -P` from a POST_BUILD step. required -D variables:
|
||||
# OBJDUMP - path to objdump (CMAKE_OBJDUMP)
|
||||
# TARGET_FILE - path to the PE binary to inspect
|
||||
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
|
||||
|
||||
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
|
||||
message(WARNING
|
||||
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
|
||||
OUTPUT_VARIABLE dump_output
|
||||
RESULT_VARIABLE dump_result
|
||||
ERROR_VARIABLE dump_error)
|
||||
|
||||
if(NOT dump_result EQUAL 0)
|
||||
message(WARNING
|
||||
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
|
||||
# statically imported DLL in their PE private-header dump.
|
||||
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
|
||||
|
||||
set(violations "")
|
||||
foreach(line IN LISTS dll_lines)
|
||||
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
|
||||
string(STRIP "${dll_name}" dll_name)
|
||||
string(TOLOWER "${dll_name}" dll_name_lower)
|
||||
if(dll_name_lower IN_LIST FORBIDDEN)
|
||||
list(APPEND violations "${dll_name}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(violations)
|
||||
list(REMOVE_DUPLICATES violations)
|
||||
string(REPLACE ";" ", " violations_str "${violations}")
|
||||
message(FATAL_ERROR
|
||||
"static DLL import check FAILED for ${TARGET_FILE}\n"
|
||||
" forbidden static imports found: ${violations_str}\n"
|
||||
"\n"
|
||||
" these DLLs must never be statically imported by spice:\n"
|
||||
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
|
||||
" system copy at startup and preempts the modules override (issue #779).\n"
|
||||
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
|
||||
" Unity games.\n"
|
||||
"\n"
|
||||
" fix: load the DLL dynamically instead - replace the direct API call with a\n"
|
||||
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
|
||||
" call through the resolved function pointer.")
|
||||
endif()
|
||||
|
||||
message(STATUS "static DLL import check passed for ${TARGET_FILE}")
|
||||
|
||||
+10
@@ -7,6 +7,7 @@ RUN pacman --noconfirm -Syu git \
|
||||
ninja \
|
||||
cmake \
|
||||
unzip \
|
||||
nasm \
|
||||
mingw-w64-crt \
|
||||
mingw-w64-winpthreads \
|
||||
mingw-w64-gcc \
|
||||
@@ -23,3 +24,12 @@ ENV PATH="$PATH:/opt/llvm-mingw-xp/bin"
|
||||
|
||||
RUN curl -fsSL "https://github.com/mon/windows-dll-compat-checker/releases/download/v1.3/windows_dll_compat_checker-linux-x86_64.tar.xz" \
|
||||
| tar -xJ -C /usr/local/bin
|
||||
|
||||
# Stock makepkg.conf builds serially; this makes the AUR compiles below parallel.
|
||||
RUN printf '%s\n' 'MAKEFLAGS="-j$(nproc)"' > /home/user/.makepkg.conf \
|
||||
&& chown user: /home/user/.makepkg.conf
|
||||
|
||||
# libjpeg-turbo for JPEG encoding, x264 for the API H.264 video stream. Only the
|
||||
# mingw-w64 toolchains get these; the WinXP targets build without JPEG support
|
||||
# and without the stream encoder.
|
||||
RUN su user -c "yay --noconfirm -S mingw-w64-libjpeg-turbo mingw-w64-x264"
|
||||
|
||||
+95
-95
@@ -1,95 +1,95 @@
|
||||
Copyright (c) 2017, keshikan (http://www.keshikan.net),
|
||||
with Reserved Font Name "DSEG".
|
||||
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Copyright (c) 2017, keshikan (http://www.keshikan.net),
|
||||
with Reserved Font Name "DSEG".
|
||||
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
+11023
-11023
File diff suppressed because one or more lines are too long
+552
-552
File diff suppressed because it is too large
Load Diff
+73
-73
@@ -1,73 +1,73 @@
|
||||
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||
|
||||
// This code comes from:
|
||||
// https://github.com/dhbaird/easywsclient
|
||||
//
|
||||
// To get the latest version:
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace easywsclient {
|
||||
|
||||
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
|
||||
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
|
||||
|
||||
class WebSocket {
|
||||
public:
|
||||
typedef WebSocket * pointer;
|
||||
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
|
||||
|
||||
// Factories:
|
||||
static pointer create_dummy();
|
||||
static pointer from_url(const std::string& url, const std::string& origin = std::string());
|
||||
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
|
||||
|
||||
// Interfaces:
|
||||
virtual ~WebSocket() { }
|
||||
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
|
||||
virtual void send(const std::string& message) = 0;
|
||||
virtual void sendBinary(const std::string& message) = 0;
|
||||
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
|
||||
virtual void sendPing() = 0;
|
||||
virtual void close() = 0;
|
||||
virtual readyStateValues getReadyState() const = 0;
|
||||
|
||||
template<class Callable>
|
||||
void dispatch(Callable callable)
|
||||
// For callbacks that accept a string argument.
|
||||
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||
struct _Callback : public Callback_Imp {
|
||||
Callable& callable;
|
||||
_Callback(Callable& callable) : callable(callable) { }
|
||||
void operator()(const std::string& message) { callable(message); }
|
||||
};
|
||||
_Callback callback(callable);
|
||||
_dispatch(callback);
|
||||
}
|
||||
|
||||
template<class Callable>
|
||||
void dispatchBinary(Callable callable)
|
||||
// For callbacks that accept a std::vector<uint8_t> argument.
|
||||
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||
struct _Callback : public BytesCallback_Imp {
|
||||
Callable& callable;
|
||||
_Callback(Callable& callable) : callable(callable) { }
|
||||
void operator()(const std::vector<uint8_t>& message) { callable(message); }
|
||||
};
|
||||
_Callback callback(callable);
|
||||
_dispatchBinary(callback);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void _dispatch(Callback_Imp& callable) = 0;
|
||||
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
|
||||
};
|
||||
|
||||
} // namespace easywsclient
|
||||
|
||||
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
|
||||
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
|
||||
|
||||
// This code comes from:
|
||||
// https://github.com/dhbaird/easywsclient
|
||||
//
|
||||
// To get the latest version:
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
|
||||
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace easywsclient {
|
||||
|
||||
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
|
||||
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
|
||||
|
||||
class WebSocket {
|
||||
public:
|
||||
typedef WebSocket * pointer;
|
||||
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
|
||||
|
||||
// Factories:
|
||||
static pointer create_dummy();
|
||||
static pointer from_url(const std::string& url, const std::string& origin = std::string());
|
||||
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
|
||||
|
||||
// Interfaces:
|
||||
virtual ~WebSocket() { }
|
||||
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
|
||||
virtual void send(const std::string& message) = 0;
|
||||
virtual void sendBinary(const std::string& message) = 0;
|
||||
virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
|
||||
virtual void sendPing() = 0;
|
||||
virtual void close() = 0;
|
||||
virtual readyStateValues getReadyState() const = 0;
|
||||
|
||||
template<class Callable>
|
||||
void dispatch(Callable callable)
|
||||
// For callbacks that accept a string argument.
|
||||
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||
struct _Callback : public Callback_Imp {
|
||||
Callable& callable;
|
||||
_Callback(Callable& callable) : callable(callable) { }
|
||||
void operator()(const std::string& message) { callable(message); }
|
||||
};
|
||||
_Callback callback(callable);
|
||||
_dispatch(callback);
|
||||
}
|
||||
|
||||
template<class Callable>
|
||||
void dispatchBinary(Callable callable)
|
||||
// For callbacks that accept a std::vector<uint8_t> argument.
|
||||
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
|
||||
struct _Callback : public BytesCallback_Imp {
|
||||
Callable& callable;
|
||||
_Callback(Callable& callable) : callable(callable) { }
|
||||
void operator()(const std::vector<uint8_t>& message) { callable(message); }
|
||||
};
|
||||
_Callback callback(callable);
|
||||
_dispatchBinary(callback);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void _dispatch(Callback_Imp& callable) = 0;
|
||||
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
|
||||
};
|
||||
|
||||
} // namespace easywsclient
|
||||
|
||||
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
|
||||
|
||||
Vendored
+3222
File diff suppressed because it is too large
Load Diff
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
// fpng.h - unlicense (see end of fpng.cpp)
|
||||
#pragma once
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
#ifndef FPNG_TRAIN_HUFFMAN_TABLES
|
||||
// Set to 1 when using the -t (training) option in fpng_test to generate new opaque/alpha Huffman tables for the single pass encoder.
|
||||
#define FPNG_TRAIN_HUFFMAN_TABLES (0)
|
||||
#endif
|
||||
|
||||
namespace fpng
|
||||
{
|
||||
// ---- Library initialization - call once to identify if the processor supports SSE.
|
||||
// Otherwise you'll only get scalar fallbacks.
|
||||
void fpng_init();
|
||||
|
||||
// ---- Useful Utilities
|
||||
|
||||
// Returns true if the CPU supports SSE 4.1, and SSE support wasn't disabled by setting FPNG_NO_SSE=1.
|
||||
// fpng_init() must have been called first, or it'll assert and return false.
|
||||
bool fpng_cpu_supports_sse41();
|
||||
|
||||
// Fast CRC-32 SSE4.1+pclmul or a scalar fallback (slice by 4)
|
||||
const uint32_t FPNG_CRC32_INIT = 0;
|
||||
uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32 = FPNG_CRC32_INIT);
|
||||
|
||||
// Fast Adler32 SSE4.1 Adler-32 with a scalar fallback.
|
||||
const uint32_t FPNG_ADLER32_INIT = 1;
|
||||
uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler = FPNG_ADLER32_INIT);
|
||||
|
||||
// ---- Compression
|
||||
enum
|
||||
{
|
||||
// Enables computing custom Huffman tables for each file, instead of using the custom global tables.
|
||||
// Results in roughly 6% smaller files on average, but compression is around 40% slower.
|
||||
FPNG_ENCODE_SLOWER = 1,
|
||||
|
||||
// Only use raw Deflate blocks (no compression at all). Intended for testing.
|
||||
FPNG_FORCE_UNCOMPRESSED = 2,
|
||||
};
|
||||
|
||||
// Fast PNG encoding. The resulting file can be decoded either using a standard PNG decoder or by the fpng_decode_memory() function below.
|
||||
// pImage: pointer to RGB or RGBA image pixels, R first in memory, B/A last.
|
||||
// w/h - image dimensions. Image's row pitch in bytes must is w*num_chans.
|
||||
// num_chans must be 3 or 4.
|
||||
bool fpng_encode_image_to_memory(const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, std::vector<uint8_t>& out_buf, uint32_t flags = 0);
|
||||
|
||||
#ifndef FPNG_NO_STDIO
|
||||
// Fast PNG encoding to the specified file.
|
||||
bool fpng_encode_image_to_file(const char* pFilename, const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, uint32_t flags = 0);
|
||||
#endif
|
||||
|
||||
// ---- Decompression
|
||||
|
||||
enum
|
||||
{
|
||||
FPNG_DECODE_SUCCESS = 0, // file is a valid PNG file and written by FPNG and the decode succeeded
|
||||
|
||||
FPNG_DECODE_NOT_FPNG, // file is a valid PNG file, but it wasn't written by FPNG so you should try decoding it with a general purpose PNG decoder
|
||||
|
||||
FPNG_DECODE_INVALID_ARG, // invalid function parameter
|
||||
|
||||
FPNG_DECODE_FAILED_NOT_PNG, // file cannot be a PNG file
|
||||
FPNG_DECODE_FAILED_HEADER_CRC32, // a chunk CRC32 check failed, file is likely corrupted or not PNG
|
||||
FPNG_DECODE_FAILED_INVALID_DIMENSIONS, // invalid image dimensions in IHDR chunk (0 or too large)
|
||||
FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE, // decoding the file fully into memory would likely require too much memory (only on 32bpp builds)
|
||||
FPNG_DECODE_FAILED_CHUNK_PARSING, // failed while parsing the chunk headers, or file is corrupted
|
||||
FPNG_DECODE_FAILED_INVALID_IDAT, // IDAT data length is too small and cannot be valid, file is either corrupted or it's a bug
|
||||
|
||||
// fpng_decode_file() specific errors
|
||||
FPNG_DECODE_FILE_OPEN_FAILED,
|
||||
FPNG_DECODE_FILE_TOO_LARGE,
|
||||
FPNG_DECODE_FILE_READ_FAILED,
|
||||
FPNG_DECODE_FILE_SEEK_FAILED
|
||||
};
|
||||
|
||||
// Fast PNG decoding of files ONLY created by fpng_encode_image_to_memory() or fpng_encode_image_to_file().
|
||||
// If fpng_get_info() or fpng_decode_memory() returns FPNG_DECODE_NOT_FPNG, you should decode the PNG by falling back to a general purpose decoder.
|
||||
//
|
||||
// fpng_get_info() parses the PNG header and iterates through all chunks to determine if it's a file written by FPNG, but does not decompress the actual image data so it's relatively fast.
|
||||
//
|
||||
// pImage, image_size: Pointer to PNG image data and its size
|
||||
// width, height: output image's dimensions
|
||||
// channels_in_file: will be 3 or 4
|
||||
//
|
||||
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
|
||||
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
|
||||
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
|
||||
int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file);
|
||||
|
||||
// fpng_decode_memory() decompresses 24/32bpp PNG files ONLY encoded by this module.
|
||||
// If the image was written by FPNG, it will decompress the image data, otherwise it will return FPNG_DECODE_NOT_FPNG in which case you should fall back to a general purpose PNG decoder (lodepng, stb_image, libpng, etc.)
|
||||
//
|
||||
// pImage, image_size: Pointer to PNG image data and its size
|
||||
// out: Output 24/32bpp image buffer
|
||||
// width, height: output image's dimensions
|
||||
// channels_in_file: will be 3 or 4
|
||||
// desired_channels: must be 3 or 4
|
||||
//
|
||||
// If the image is 24bpp and 32bpp is requested, the alpha values will be set to 0xFF.
|
||||
// If the image is 32bpp and 24bpp is requested, the alpha values will be discarded.
|
||||
//
|
||||
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
|
||||
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
|
||||
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
|
||||
int fpng_decode_memory(const void* pImage, uint32_t image_size, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
|
||||
|
||||
#ifndef FPNG_NO_STDIO
|
||||
int fpng_decode_file(const char* pFilename, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
|
||||
#endif
|
||||
|
||||
// ---- Internal API used for Huffman table training purposes
|
||||
|
||||
#if FPNG_TRAIN_HUFFMAN_TABLES
|
||||
const uint32_t HUFF_COUNTS_SIZE = 288;
|
||||
extern uint64_t g_huff_counts[HUFF_COUNTS_SIZE];
|
||||
bool create_dynamic_block_prefix(uint64_t* pFreq, uint32_t num_chans, std::vector<uint8_t>& prefix, uint64_t& bit_buf, int& bit_buf_size, uint32_t *pCodes, uint8_t *pCodesizes);
|
||||
#endif
|
||||
|
||||
} // namespace fpng
|
||||
Vendored
-2192
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -878,7 +878,15 @@ inline void ImGui::FileBrowser::UpdateFileRecords()
|
||||
|
||||
inline void ImGui::FileBrowser::SetPwdUncatched(const std::filesystem::path &pwd)
|
||||
{
|
||||
pwd_ = absolute(pwd);
|
||||
// spice2x
|
||||
// avoid MinGW std::filesystem::absolute() duplicating UNC server/share
|
||||
// prefixes, which makes directory_iterator() throw during startup
|
||||
// spice2x
|
||||
const auto &nativePwd = pwd.native();
|
||||
const bool hasUncPrefix = nativePwd.size() >= 2 &&
|
||||
(nativePwd[0] == '\\' || nativePwd[0] == '/') &&
|
||||
(nativePwd[1] == '\\' || nativePwd[1] == '/');
|
||||
pwd_ = hasUncPrefix ? pwd : absolute(pwd);
|
||||
UpdateFileRecords();
|
||||
selectedFilenames_.clear();
|
||||
(*inputNameBuf_)[0] = '\0';
|
||||
|
||||
+835
-835
File diff suppressed because it is too large
Load Diff
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
zlib License
|
||||
|
||||
Copyright (c) 2011-2016 Stephan Brumme
|
||||
|
||||
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
|
||||
If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
-665
@@ -1,665 +0,0 @@
|
||||
// //////////////////////////////////////////////////////////
|
||||
// toojpeg.cpp
|
||||
// written by Stephan Brumme, 2018-2019
|
||||
// see https://create.stephan-brumme.com/toojpeg/
|
||||
//
|
||||
|
||||
#include "toojpeg.h"
|
||||
|
||||
// - the "official" specifications: https://www.w3.org/Graphics/JPEG/itu-t81.pdf and https://www.w3.org/Graphics/JPEG/jfif3.pdf
|
||||
// - Wikipedia has a short description of the JFIF/JPEG file format: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
|
||||
// - the popular STB Image library includes Jon's JPEG encoder as well: https://github.com/nothings/stb/blob/master/stb_image_write.h
|
||||
// - the most readable JPEG book (from a developer's perspective) is Miano's "Compressed Image File Formats" (1999, ISBN 0-201-60443-4),
|
||||
// used copies are really cheap nowadays and include a CD with C++ sources as well (plus great format descriptions of GIF & PNG)
|
||||
// - much more detailled is Mitchell/Pennebaker's "JPEG: Still Image Data Compression Standard" (1993, ISBN 0-442-01272-1)
|
||||
// which contains the official JPEG standard, too - fun fact: I bought a signed copy in a second-hand store without noticing
|
||||
|
||||
namespace // anonymous namespace to hide local functions / constants / etc.
|
||||
{
|
||||
// ////////////////////////////////////////
|
||||
// data types
|
||||
using uint8_t = unsigned char;
|
||||
using uint16_t = unsigned short;
|
||||
using int16_t = short;
|
||||
using int32_t = int; // at least four bytes
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// constants
|
||||
|
||||
// quantization tables from JPEG Standard, Annex K
|
||||
const uint8_t DefaultQuantLuminance[8*8] =
|
||||
{ 16, 11, 10, 16, 24, 40, 51, 61, // there are a few experts proposing slightly more efficient values,
|
||||
12, 12, 14, 19, 26, 58, 60, 55, // e.g. https://www.imagemagick.org/discourse-server/viewtopic.php?t=20333
|
||||
14, 13, 16, 24, 40, 57, 69, 56, // btw: Google's Guetzli project optimizes the quantization tables per image
|
||||
14, 17, 22, 29, 51, 87, 80, 62,
|
||||
18, 22, 37, 56, 68,109,103, 77,
|
||||
24, 35, 55, 64, 81,104,113, 92,
|
||||
49, 64, 78, 87,103,121,120,101,
|
||||
72, 92, 95, 98,112,100,103, 99 };
|
||||
const uint8_t DefaultQuantChrominance[8*8] =
|
||||
{ 17, 18, 24, 47, 99, 99, 99, 99,
|
||||
18, 21, 26, 66, 99, 99, 99, 99,
|
||||
24, 26, 56, 99, 99, 99, 99, 99,
|
||||
47, 66, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99,
|
||||
99, 99, 99, 99, 99, 99, 99, 99 };
|
||||
|
||||
// 8x8 blocks are processed in zig-zag order
|
||||
// most encoders use a zig-zag "forward" table, I switched to its inverse for performance reasons
|
||||
// note: ZigZagInv[ZigZag[i]] = i
|
||||
const uint8_t ZigZagInv[8*8] =
|
||||
{ 0, 1, 8,16, 9, 2, 3,10, // ZigZag[] = 0, 1, 5, 6,14,15,27,28,
|
||||
17,24,32,25,18,11, 4, 5, // 2, 4, 7,13,16,26,29,42,
|
||||
12,19,26,33,40,48,41,34, // 3, 8,12,17,25,30,41,43,
|
||||
27,20,13, 6, 7,14,21,28, // 9,11,18,24,31,40,44,53,
|
||||
35,42,49,56,57,50,43,36, // 10,19,23,32,39,45,52,54,
|
||||
29,22,15,23,30,37,44,51, // 20,22,33,38,46,51,55,60,
|
||||
58,59,52,45,38,31,39,46, // 21,34,37,47,50,56,59,61,
|
||||
53,60,61,54,47,55,62,63 }; // 35,36,48,49,57,58,62,63
|
||||
|
||||
// static Huffman code tables from JPEG standard Annex K
|
||||
// - CodesPerBitsize tables define how many Huffman codes will have a certain bitsize (plus 1 because there nothing with zero bits),
|
||||
// e.g. DcLuminanceCodesPerBitsize[2] = 5 because there are 5 Huffman codes being 2+1=3 bits long
|
||||
// - Values tables are a list of values ordered by their Huffman code bitsize,
|
||||
// e.g. AcLuminanceValues => Huffman(0x01,0x02 and 0x03) will have 2 bits, Huffman(0x00) will have 3 bits, Huffman(0x04,0x11 and 0x05) will have 4 bits, ...
|
||||
|
||||
// Huffman definitions for first DC/AC tables (luminance / Y channel)
|
||||
const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12
|
||||
const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes
|
||||
const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162
|
||||
const uint8_t AcLuminanceValues [162] = // => 162 codes
|
||||
{ 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because
|
||||
0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F
|
||||
0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A
|
||||
0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0
|
||||
0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee
|
||||
0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2,
|
||||
0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
|
||||
// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels)
|
||||
const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12
|
||||
const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues)
|
||||
const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162
|
||||
const uint8_t AcChrominanceValues [162] = // => 162 codes
|
||||
{ 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order
|
||||
0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding)
|
||||
0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,
|
||||
0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87,
|
||||
0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,
|
||||
0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,
|
||||
0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
|
||||
const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// structs
|
||||
|
||||
// represent a single Huffman code
|
||||
struct BitCode
|
||||
{
|
||||
BitCode() = default; // undefined state, must be initialized at a later time
|
||||
BitCode(uint16_t code_, uint8_t numBits_)
|
||||
: code(code_), numBits(numBits_) {}
|
||||
uint16_t code; // JPEG's Huffman codes are limited to 16 bits
|
||||
uint8_t numBits; // number of valid bits
|
||||
};
|
||||
|
||||
// wrapper for bit output operations
|
||||
struct BitWriter
|
||||
{
|
||||
// user-supplied callback that writes/stores one byte
|
||||
TooJpeg::WRITE_ONE_BYTE output;
|
||||
// initialize writer
|
||||
explicit BitWriter(TooJpeg::WRITE_ONE_BYTE output_) : output(output_) {}
|
||||
|
||||
// store the most recently encoded bits that are not written yet
|
||||
struct BitBuffer
|
||||
{
|
||||
int32_t data = 0; // actually only at most 24 bits are used
|
||||
uint8_t numBits = 0; // number of valid bits (the right-most bits)
|
||||
} buffer;
|
||||
|
||||
// write Huffman bits stored in BitCode, keep excess bits in BitBuffer
|
||||
BitWriter& operator<<(const BitCode& data)
|
||||
{
|
||||
// append the new bits to those bits leftover from previous call(s)
|
||||
buffer.numBits += data.numBits;
|
||||
buffer.data <<= data.numBits;
|
||||
buffer.data |= data.code;
|
||||
|
||||
// write all "full" bytes
|
||||
while (buffer.numBits >= 8)
|
||||
{
|
||||
// extract highest 8 bits
|
||||
buffer.numBits -= 8;
|
||||
auto oneByte = uint8_t(buffer.data >> buffer.numBits);
|
||||
output(oneByte);
|
||||
|
||||
if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker)
|
||||
output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence"
|
||||
|
||||
// note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits
|
||||
// if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line
|
||||
//buffer.bits &= (1 << buffer.numBits) - 1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing)
|
||||
void flush()
|
||||
{
|
||||
// at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111
|
||||
*this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it
|
||||
}
|
||||
|
||||
// NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output !
|
||||
// write a single byte
|
||||
BitWriter& operator<<(uint8_t oneByte)
|
||||
{
|
||||
output(oneByte);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// write an array of bytes
|
||||
template <typename T, int Size>
|
||||
BitWriter& operator<<(T (&manyBytes)[Size])
|
||||
{
|
||||
for (auto c : manyBytes)
|
||||
output(c);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// start a new JFIF block
|
||||
void addMarker(uint8_t id, uint16_t length)
|
||||
{
|
||||
output(0xFF); output(id); // ID, always preceded by 0xFF
|
||||
output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well)
|
||||
output(uint8_t(length & 0xFF));
|
||||
}
|
||||
};
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// functions / templates
|
||||
|
||||
// same as std::min()
|
||||
template <typename Number>
|
||||
Number minimum(Number value, Number maximum)
|
||||
{
|
||||
return value <= maximum ? value : maximum;
|
||||
}
|
||||
|
||||
// restrict a value to the interval [minimum, maximum]
|
||||
template <typename Number, typename Limit>
|
||||
Number clamp(Number value, Limit minValue, Limit maxValue)
|
||||
{
|
||||
if (value <= minValue) return minValue; // never smaller than the minimum
|
||||
if (value >= maxValue) return maxValue; // never bigger than the maximum
|
||||
return value; // value was inside interval, keep it
|
||||
}
|
||||
|
||||
// convert from RGB to YCbCr, constants are similar to ITU-R, see https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion
|
||||
float rgb2y (float r, float g, float b) { return +0.299f * r +0.587f * g +0.114f * b; }
|
||||
float rgb2cb(float r, float g, float b) { return -0.16874f * r -0.33126f * g +0.5f * b; }
|
||||
float rgb2cr(float r, float g, float b) { return +0.5f * r -0.41869f * g -0.08131f * b; }
|
||||
|
||||
// forward DCT computation "in one dimension" (fast AAN algorithm by Arai, Agui and Nakajima: "A fast DCT-SQ scheme for images")
|
||||
void DCT(float block[8*8], uint8_t stride) // stride must be 1 (=horizontal) or 8 (=vertical)
|
||||
{
|
||||
const auto SqrtHalfSqrt = 1.306562965f; // sqrt((2 + sqrt(2)) / 2) = cos(pi * 1 / 8) * sqrt(2)
|
||||
const auto InvSqrt = 0.707106781f; // 1 / sqrt(2) = cos(pi * 2 / 8)
|
||||
const auto HalfSqrtSqrt = 0.382683432f; // sqrt(2 - sqrt(2)) / 2 = cos(pi * 3 / 8)
|
||||
const auto InvSqrtSqrt = 0.541196100f; // 1 / sqrt(2 - sqrt(2)) = cos(pi * 3 / 8) * sqrt(2)
|
||||
|
||||
// modify in-place
|
||||
auto& block0 = block[0 ];
|
||||
auto& block1 = block[1 * stride];
|
||||
auto& block2 = block[2 * stride];
|
||||
auto& block3 = block[3 * stride];
|
||||
auto& block4 = block[4 * stride];
|
||||
auto& block5 = block[5 * stride];
|
||||
auto& block6 = block[6 * stride];
|
||||
auto& block7 = block[7 * stride];
|
||||
|
||||
// based on https://dev.w3.org/Amaya/libjpeg/jfdctflt.c , the original variable names can be found in my comments
|
||||
auto add07 = block0 + block7; auto sub07 = block0 - block7; // tmp0, tmp7
|
||||
auto add16 = block1 + block6; auto sub16 = block1 - block6; // tmp1, tmp6
|
||||
auto add25 = block2 + block5; auto sub25 = block2 - block5; // tmp2, tmp5
|
||||
auto add34 = block3 + block4; auto sub34 = block3 - block4; // tmp3, tmp4
|
||||
|
||||
auto add0347 = add07 + add34; auto sub07_34 = add07 - add34; // tmp10, tmp13 ("even part" / "phase 2")
|
||||
auto add1256 = add16 + add25; auto sub16_25 = add16 - add25; // tmp11, tmp12
|
||||
|
||||
block0 = add0347 + add1256; block4 = add0347 - add1256; // "phase 3"
|
||||
|
||||
auto z1 = (sub16_25 + sub07_34) * InvSqrt; // all temporary z-variables kept their original names
|
||||
block2 = sub07_34 + z1; block6 = sub07_34 - z1; // "phase 5"
|
||||
|
||||
auto sub23_45 = sub25 + sub34; // tmp10 ("odd part" / "phase 2")
|
||||
auto sub12_56 = sub16 + sub25; // tmp11
|
||||
auto sub01_67 = sub16 + sub07; // tmp12
|
||||
|
||||
auto z5 = (sub23_45 - sub01_67) * HalfSqrtSqrt;
|
||||
auto z2 = sub23_45 * InvSqrtSqrt + z5;
|
||||
auto z3 = sub12_56 * InvSqrt;
|
||||
auto z4 = sub01_67 * SqrtHalfSqrt + z5;
|
||||
auto z6 = sub07 + z3; // z11 ("phase 5")
|
||||
auto z7 = sub07 - z3; // z13
|
||||
block1 = z6 + z4; block7 = z6 - z4; // "phase 6"
|
||||
block5 = z7 + z2; block3 = z7 - z2;
|
||||
}
|
||||
|
||||
// run DCT, quantize and write Huffman bit codes
|
||||
int16_t encodeBlock(BitWriter& writer, float block[8][8], const float scaled[8*8], int16_t lastDC,
|
||||
const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords)
|
||||
{
|
||||
// "linearize" the 8x8 block, treat it as a flat array of 64 floats
|
||||
auto block64 = (float*) block;
|
||||
|
||||
// DCT: rows
|
||||
for (auto offset = 0; offset < 8; offset++)
|
||||
DCT(block64 + offset*8, 1);
|
||||
// DCT: columns
|
||||
for (auto offset = 0; offset < 8; offset++)
|
||||
DCT(block64 + offset*1, 8);
|
||||
|
||||
// scale
|
||||
for (auto i = 0; i < 8*8; i++)
|
||||
block64[i] *= scaled[i];
|
||||
|
||||
// encode DC (the first coefficient is the "average color" of the 8x8 block)
|
||||
auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
|
||||
|
||||
// quantize and zigzag the other 63 coefficients
|
||||
auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently)
|
||||
int16_t quantized[8*8];
|
||||
for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed
|
||||
{
|
||||
auto value = block64[ZigZagInv[i]];
|
||||
// round to nearest integer
|
||||
quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
|
||||
// remember offset of last non-zero coefficient
|
||||
if (quantized[i] != 0)
|
||||
posNonZero = i;
|
||||
}
|
||||
|
||||
// same "average color" as previous block ?
|
||||
auto diff = DC - lastDC;
|
||||
if (diff == 0)
|
||||
writer << huffmanDC[0x00]; // yes, write a special short symbol
|
||||
else
|
||||
{
|
||||
auto bits = codewords[diff]; // nope, encode the difference to previous block's average color
|
||||
writer << huffmanDC[bits.numBits] << bits;
|
||||
}
|
||||
|
||||
// encode ACs (quantized[1..63])
|
||||
auto offset = 0; // upper 4 bits count the number of consecutive zeros
|
||||
for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too
|
||||
{
|
||||
// zeros are encoded in a special way
|
||||
while (quantized[i] == 0) // found another zero ?
|
||||
{
|
||||
offset += 0x10; // add 1 to the upper 4 bits
|
||||
// split into blocks of at most 16 consecutive zeros
|
||||
if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15
|
||||
{
|
||||
writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros"
|
||||
offset = 0;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
auto encoded = codewords[quantized[i]];
|
||||
// combine number of zeros with the number of bits of the next non-zero value
|
||||
writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
// send end-of-block code (0x00), only needed if there are trailing zeros
|
||||
if (posNonZero < 8*8 - 1) // = 63
|
||||
writer << huffmanAC[0x00];
|
||||
|
||||
return DC;
|
||||
}
|
||||
|
||||
// Jon's code includes the pre-generated Huffman codes
|
||||
// I don't like these "magic constants" and compute them on my own :-)
|
||||
void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256])
|
||||
{
|
||||
// process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits
|
||||
auto huffmanCode = 0;
|
||||
for (auto numBits = 1; numBits <= 16; numBits++)
|
||||
{
|
||||
// ... and each code of these bitsizes
|
||||
for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1
|
||||
result[*values++] = BitCode(huffmanCode++, numBits);
|
||||
|
||||
// next Huffman code needs to be one bit wider
|
||||
huffmanCode <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
} // end of anonymous namespace
|
||||
|
||||
// -------------------- externally visible code --------------------
|
||||
|
||||
namespace TooJpeg
|
||||
{
|
||||
// the only exported function ...
|
||||
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels_, unsigned short width, unsigned short height,
|
||||
bool isRGB, unsigned char quality_, bool downsample, const char* comment)
|
||||
{
|
||||
// reject invalid pointers
|
||||
if (output == nullptr || pixels_ == nullptr)
|
||||
return false;
|
||||
// check image format
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
|
||||
// number of components
|
||||
const auto numComponents = isRGB ? 3 : 1;
|
||||
// note: if there is just one component (=grayscale), then only luminance needs to be stored in the file
|
||||
// thus everything related to chrominance need not to be written to the JPEG
|
||||
// I still compute a few things, like quantization tables to avoid a complete code mess
|
||||
|
||||
// grayscale images can't be downsampled (because there are no Cb + Cr channels)
|
||||
if (!isRGB)
|
||||
downsample = false;
|
||||
|
||||
// wrapper for all output operations
|
||||
BitWriter bitWriter(output);
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// JFIF headers
|
||||
const uint8_t HeaderJfif[2+2+16] =
|
||||
{ 0xFF,0xD8, // SOI marker (start of image)
|
||||
0xFF,0xE0, // JFIF APP0 tag
|
||||
0,16, // length: 16 bytes (14 bytes payload + 2 bytes for this length field)
|
||||
'J','F','I','F',0, // JFIF identifier, zero-terminated
|
||||
1,1, // JFIF version 1.1
|
||||
0, // no density units specified
|
||||
0,1,0,1, // density: 1 pixel "per pixel" horizontally and vertically
|
||||
0,0 }; // no thumbnail (size 0 x 0)
|
||||
bitWriter << HeaderJfif;
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// comment (optional)
|
||||
if (comment != nullptr)
|
||||
{
|
||||
// look for zero terminator
|
||||
auto length = 0; // = strlen(comment);
|
||||
while (comment[length] != 0)
|
||||
length++;
|
||||
|
||||
// write COM marker
|
||||
bitWriter.addMarker(0xFE, 2+length); // block size is number of bytes (without zero terminator) + 2 bytes for this length field
|
||||
// ... and write the comment itself
|
||||
for (auto i = 0; i < length; i++)
|
||||
bitWriter << comment[i];
|
||||
}
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// adjust quantization tables to desired quality
|
||||
|
||||
// quality level must be in 1 ... 100
|
||||
auto quality = clamp<uint16_t>(quality_, 1, 100);
|
||||
// convert to an internal JPEG quality factor, formula taken from libjpeg
|
||||
quality = quality < 50 ? 5000 / quality : 200 - quality * 2;
|
||||
|
||||
uint8_t quantLuminance [8*8];
|
||||
uint8_t quantChrominance[8*8];
|
||||
for (auto i = 0; i < 8*8; i++)
|
||||
{
|
||||
int luminance = (DefaultQuantLuminance [ZigZagInv[i]] * quality + 50) / 100;
|
||||
int chrominance = (DefaultQuantChrominance[ZigZagInv[i]] * quality + 50) / 100;
|
||||
|
||||
// clamp to 1..255
|
||||
quantLuminance [i] = clamp(luminance, 1, 255);
|
||||
quantChrominance[i] = clamp(chrominance, 1, 255);
|
||||
}
|
||||
|
||||
// write quantization tables
|
||||
bitWriter.addMarker(0xDB, 2 + (isRGB ? 2 : 1) * (1 + 8*8)); // length: 65 bytes per table + 2 bytes for this length field
|
||||
// each table has 64 entries and is preceded by an ID byte
|
||||
|
||||
bitWriter << 0x00 << quantLuminance; // first quantization table
|
||||
if (isRGB)
|
||||
bitWriter << 0x01 << quantChrominance; // second quantization table, only relevant for color images
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// write image infos (SOF0 - start of frame)
|
||||
bitWriter.addMarker(0xC0, 2+6+3*numComponents); // length: 6 bytes general info + 3 per channel + 2 bytes for this length field
|
||||
|
||||
// 8 bits per channel
|
||||
bitWriter << 0x08
|
||||
// image dimensions (big-endian)
|
||||
<< (height >> 8) << (height & 0xFF)
|
||||
<< (width >> 8) << (width & 0xFF);
|
||||
|
||||
// sampling and quantization tables for each component
|
||||
bitWriter << numComponents; // 1 component (grayscale, Y only) or 3 components (Y,Cb,Cr)
|
||||
for (auto id = 1; id <= numComponents; id++)
|
||||
bitWriter << id // component ID (Y=1, Cb=2, Cr=3)
|
||||
// bitmasks for sampling: highest 4 bits: horizontal, lowest 4 bits: vertical
|
||||
<< (id == 1 && downsample ? 0x22 : 0x11) // 0x11 is default YCbCr 4:4:4 and 0x22 stands for YCbCr 4:2:0
|
||||
<< (id == 1 ? 0 : 1); // use quantization table 0 for Y, table 1 for Cb and Cr
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// Huffman tables
|
||||
// DHT marker - define Huffman tables
|
||||
bitWriter.addMarker(0xC4, isRGB ? (2+208+208) : (2+208));
|
||||
// 2 bytes for the length field, store chrominance only if needed
|
||||
// 1+16+12 for the DC luminance
|
||||
// 1+16+162 for the AC luminance (208 = 1+16+12 + 1+16+162)
|
||||
// 1+16+12 for the DC chrominance
|
||||
// 1+16+162 for the AC chrominance (208 = 1+16+12 + 1+16+162, same as above)
|
||||
|
||||
// store luminance's DC+AC Huffman table definitions
|
||||
bitWriter << 0x00 // highest 4 bits: 0 => DC, lowest 4 bits: 0 => Y (baseline)
|
||||
<< DcLuminanceCodesPerBitsize
|
||||
<< DcLuminanceValues;
|
||||
bitWriter << 0x10 // highest 4 bits: 1 => AC, lowest 4 bits: 0 => Y (baseline)
|
||||
<< AcLuminanceCodesPerBitsize
|
||||
<< AcLuminanceValues;
|
||||
|
||||
// compute actual Huffman code tables (see Jon's code for precalculated tables)
|
||||
BitCode huffmanLuminanceDC[256];
|
||||
BitCode huffmanLuminanceAC[256];
|
||||
generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC);
|
||||
generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC);
|
||||
|
||||
// chrominance is only relevant for color images
|
||||
BitCode huffmanChrominanceDC[256];
|
||||
BitCode huffmanChrominanceAC[256];
|
||||
if (isRGB)
|
||||
{
|
||||
// store luminance's DC+AC Huffman table definitions
|
||||
bitWriter << 0x01 // highest 4 bits: 0 => DC, lowest 4 bits: 1 => Cr,Cb (baseline)
|
||||
<< DcChrominanceCodesPerBitsize
|
||||
<< DcChrominanceValues;
|
||||
bitWriter << 0x11 // highest 4 bits: 1 => AC, lowest 4 bits: 1 => Cr,Cb (baseline)
|
||||
<< AcChrominanceCodesPerBitsize
|
||||
<< AcChrominanceValues;
|
||||
|
||||
// compute actual Huffman code tables (see Jon's code for precalculated tables)
|
||||
generateHuffmanTable(DcChrominanceCodesPerBitsize, DcChrominanceValues, huffmanChrominanceDC);
|
||||
generateHuffmanTable(AcChrominanceCodesPerBitsize, AcChrominanceValues, huffmanChrominanceAC);
|
||||
}
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// start of scan (there is only a single scan for baseline JPEGs)
|
||||
bitWriter.addMarker(0xDA, 2+1+2*numComponents+3); // 2 bytes for the length field, 1 byte for number of components,
|
||||
// then 2 bytes for each component and 3 bytes for spectral selection
|
||||
|
||||
// assign Huffman tables to each component
|
||||
bitWriter << numComponents;
|
||||
for (auto id = 1; id <= numComponents; id++)
|
||||
// highest 4 bits: DC Huffman table, lowest 4 bits: AC Huffman table
|
||||
bitWriter << id << (id == 1 ? 0x00 : 0x11); // Y: tables 0 for DC and AC; Cb + Cr: tables 1 for DC and AC
|
||||
|
||||
// constant values for our baseline JPEGs (which have a single sequential scan)
|
||||
static const uint8_t Spectral[3] = { 0, 63, 0 }; // spectral selection: must be from 0 to 63; successive approximation must be 0
|
||||
bitWriter << Spectral;
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// adjust quantization tables with AAN scaling factors to simplify DCT
|
||||
float scaledLuminance [8*8];
|
||||
float scaledChrominance[8*8];
|
||||
for (auto i = 0; i < 8*8; i++)
|
||||
{
|
||||
auto row = ZigZagInv[i] / 8; // same as ZigZagInv[i] >> 3
|
||||
auto column = ZigZagInv[i] % 8; // same as ZigZagInv[i] & 7
|
||||
|
||||
// scaling constants for AAN DCT algorithm: AanScaleFactors[0] = 1, AanScaleFactors[k=1..7] = cos(k*PI/16) * sqrt(2)
|
||||
static const float AanScaleFactors[8] = { 1, 1.387039845f, 1.306562965f, 1.175875602f, 1, 0.785694958f, 0.541196100f, 0.275899379f };
|
||||
auto factor = 1 / (AanScaleFactors[row] * AanScaleFactors[column] * 8);
|
||||
scaledLuminance [ZigZagInv[i]] = factor / quantLuminance [i];
|
||||
scaledChrominance[ZigZagInv[i]] = factor / quantChrominance[i];
|
||||
// if you really want JPEGs that are bitwise identical to Jon Olick's code then you need slightly different formulas (note: sqrt(8) = 2.828427125f)
|
||||
//static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; // line 240 of jo_jpeg.cpp
|
||||
//scaledLuminance [ZigZagInv[i]] = 1 / (quantLuminance [i] * aasf[row] * aasf[column]); // lines 266-267 of jo_jpeg.cpp
|
||||
//scaledChrominance[ZigZagInv[i]] = 1 / (quantChrominance[i] * aasf[row] * aasf[column]);
|
||||
}
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// precompute JPEG codewords for quantized DCT
|
||||
BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit]
|
||||
BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]]
|
||||
uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined)
|
||||
int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1
|
||||
for (int16_t value = 1; value < CodeWordLimit; value++)
|
||||
{
|
||||
// numBits = position of highest set bit (ignoring the sign)
|
||||
// mask = (2^numBits) - 1
|
||||
if (value > mask) // one more bit ?
|
||||
{
|
||||
numBits++;
|
||||
mask = (mask << 1) | 1; // append a set bit
|
||||
}
|
||||
codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value]
|
||||
codewords[+value] = BitCode( value, numBits);
|
||||
}
|
||||
|
||||
// just convert image data from void*
|
||||
auto pixels = (const uint8_t*)pixels_;
|
||||
|
||||
// the next two variables are frequently used when checking for image borders
|
||||
const auto maxWidth = width - 1; // "last row"
|
||||
const auto maxHeight = height - 1; // "bottom line"
|
||||
|
||||
// process MCUs (minimum codes units) => image is subdivided into a grid of 8x8 or 16x16 tiles
|
||||
const auto sampling = downsample ? 2 : 1; // 1x1 or 2x2 sampling
|
||||
const auto mcuSize = 8 * sampling;
|
||||
|
||||
// average color of the previous MCU
|
||||
int16_t lastYDC = 0, lastCbDC = 0, lastCrDC = 0;
|
||||
// convert from RGB to YCbCr
|
||||
float Y[8][8], Cb[8][8], Cr[8][8];
|
||||
|
||||
for (auto mcuY = 0; mcuY < height; mcuY += mcuSize) // each step is either 8 or 16 (=mcuSize)
|
||||
for (auto mcuX = 0; mcuX < width; mcuX += mcuSize)
|
||||
{
|
||||
// YCbCr 4:4:4 format: each MCU is a 8x8 block - the same applies to grayscale images, too
|
||||
// YCbCr 4:2:0 format: each MCU represents a 16x16 block, stored as 4x 8x8 Y-blocks plus 1x 8x8 Cb and 1x 8x8 Cr block)
|
||||
for (auto blockY = 0; blockY < mcuSize; blockY += 8) // iterate once (YCbCr444 and grayscale) or twice (YCbCr420)
|
||||
for (auto blockX = 0; blockX < mcuSize; blockX += 8)
|
||||
{
|
||||
// now we finally have an 8x8 block ...
|
||||
for (auto deltaY = 0; deltaY < 8; deltaY++)
|
||||
{
|
||||
auto column = minimum(mcuX + blockX , maxWidth); // must not exceed image borders, replicate last row/column if needed
|
||||
auto row = minimum(mcuY + blockY + deltaY, maxHeight);
|
||||
for (auto deltaX = 0; deltaX < 8; deltaX++)
|
||||
{
|
||||
// find actual pixel position within the current image
|
||||
auto pixelPos = row * int(width) + column; // the cast ensures that we don't run into multiplication overflows
|
||||
if (column < maxWidth)
|
||||
column++;
|
||||
|
||||
// grayscale images have solely a Y channel which can be easily derived from the input pixel by shifting it by 128
|
||||
if (!isRGB)
|
||||
{
|
||||
Y[deltaY][deltaX] = pixels[pixelPos] - 128.f;
|
||||
continue;
|
||||
}
|
||||
|
||||
// RGB: 3 bytes per pixel (whereas grayscale images have only 1 byte per pixel)
|
||||
auto r = pixels[3 * pixelPos ];
|
||||
auto g = pixels[3 * pixelPos + 1];
|
||||
auto b = pixels[3 * pixelPos + 2];
|
||||
|
||||
Y [deltaY][deltaX] = rgb2y (r, g, b) - 128; // again, the JPEG standard requires Y to be shifted by 128
|
||||
// YCbCr444 is easy - the more complex YCbCr420 has to be computed about 20 lines below in a second pass
|
||||
if (!downsample)
|
||||
{
|
||||
Cb[deltaY][deltaX] = rgb2cb(r, g, b); // standard RGB-to-YCbCr conversion
|
||||
Cr[deltaY][deltaX] = rgb2cr(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// encode Y channel
|
||||
lastYDC = encodeBlock(bitWriter, Y, scaledLuminance, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords);
|
||||
// Cb and Cr are encoded about 50 lines below
|
||||
}
|
||||
|
||||
// grayscale images don't need any Cb and Cr information
|
||||
if (!isRGB)
|
||||
continue;
|
||||
|
||||
// ////////////////////////////////////////
|
||||
// the following lines are only relevant for YCbCr420:
|
||||
// average/downsample chrominance of four pixels while respecting the image borders
|
||||
if (downsample)
|
||||
for (short deltaY = 7; downsample && deltaY >= 0; deltaY--) // iterating loop in reverse increases cache read efficiency
|
||||
{
|
||||
auto row = minimum(mcuY + 2*deltaY, maxHeight); // each deltaX/Y step covers a 2x2 area
|
||||
auto column = mcuX; // column is updated inside next loop
|
||||
auto pixelPos = (row * int(width) + column) * 3; // numComponents = 3
|
||||
|
||||
// deltas (in bytes) to next row / column, must not exceed image borders
|
||||
auto rowStep = (row < maxHeight) ? 3 * int(width) : 0; // always numComponents*width except for bottom line
|
||||
auto columnStep = (column < maxWidth ) ? 3 : 0; // always numComponents except for rightmost pixel
|
||||
|
||||
for (short deltaX = 0; deltaX < 8; deltaX++)
|
||||
{
|
||||
// let's add all four samples (2x2 area)
|
||||
auto right = pixelPos + columnStep;
|
||||
auto down = pixelPos + rowStep;
|
||||
auto downRight = pixelPos + columnStep + rowStep;
|
||||
|
||||
// note: cast from 8 bits to >8 bits to avoid overflows when adding
|
||||
auto r = short(pixels[pixelPos ]) + pixels[right ] + pixels[down ] + pixels[downRight ];
|
||||
auto g = short(pixels[pixelPos + 1]) + pixels[right + 1] + pixels[down + 1] + pixels[downRight + 1];
|
||||
auto b = short(pixels[pixelPos + 2]) + pixels[right + 2] + pixels[down + 2] + pixels[downRight + 2];
|
||||
|
||||
// convert to Cb and Cr
|
||||
Cb[deltaY][deltaX] = rgb2cb(r, g, b) / 4; // I still have to divide r,g,b by 4 to get their average values
|
||||
Cr[deltaY][deltaX] = rgb2cr(r, g, b) / 4; // it's a bit faster if done AFTER CbCr conversion
|
||||
|
||||
// step forward to next 2x2 area
|
||||
pixelPos += 2*3; // 2 pixels => 6 bytes (2*numComponents)
|
||||
column += 2;
|
||||
|
||||
// reached right border ?
|
||||
if (column >= maxWidth)
|
||||
{
|
||||
columnStep = 0;
|
||||
pixelPos = ((row + 1) * int(width) - 1) * 3; // same as (row * width + maxWidth) * numComponents => current's row last pixel
|
||||
}
|
||||
}
|
||||
} // end of YCbCr420 code for Cb and Cr
|
||||
|
||||
// encode Cb and Cr
|
||||
lastCbDC = encodeBlock(bitWriter, Cb, scaledChrominance, lastCbDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
|
||||
lastCrDC = encodeBlock(bitWriter, Cr, scaledChrominance, lastCrDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
|
||||
}
|
||||
|
||||
bitWriter.flush(); // now image is completely encoded, write any bits still left in the buffer
|
||||
|
||||
// ///////////////////////////
|
||||
// EOI marker
|
||||
bitWriter << 0xFF << 0xD9; // this marker has no length, therefore I can't use addMarker()
|
||||
return true;
|
||||
} // writeJpeg()
|
||||
} // namespace TooJpeg
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
// //////////////////////////////////////////////////////////
|
||||
// toojpeg.h
|
||||
// written by Stephan Brumme, 2018-2019
|
||||
// see https://create.stephan-brumme.com/toojpeg/
|
||||
//
|
||||
|
||||
// This is a compact baseline JPEG/JFIF writer, written in C++ (but looks like C for the most part).
|
||||
// Its interface has only one function: writeJpeg() - and that's it !
|
||||
//
|
||||
// basic example:
|
||||
// => create an image with any content you like, e.g. 1024x768, RGB = 3 bytes per pixel
|
||||
// auto pixels = new unsigned char[1024*768*3];
|
||||
// => you need to define a callback that receives the compressed data byte-by-byte from my JPEG writer
|
||||
// void myOutput(unsigned char oneByte) { fputc(oneByte, myFileHandle); } // save byte to file
|
||||
// => let's go !
|
||||
// TooJpeg::writeJpeg(myOutput, mypixels, 1024, 768);
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace TooJpeg
|
||||
{
|
||||
// write one byte (to disk, memory, ...)
|
||||
typedef void (*WRITE_ONE_BYTE)(unsigned char);
|
||||
// this callback is called for every byte generated by the encoder and behaves similar to fputc
|
||||
// if you prefer stylish C++11 syntax then it can be a lambda, too:
|
||||
// auto myOutput = [](unsigned char oneByte) { fputc(oneByte, output); };
|
||||
|
||||
// output - callback that stores a single byte (writes to disk, memory, ...)
|
||||
// pixels - stored in RGB format or grayscale, stored from upper-left to lower-right
|
||||
// width,height - image size
|
||||
// isRGB - true if RGB format (3 bytes per pixel); false if grayscale (1 byte per pixel)
|
||||
// quality - between 1 (worst) and 100 (best)
|
||||
// downsample - if true then YCbCr 4:2:0 format is used (smaller size, minor quality loss) instead of 4:4:4, not relevant for grayscale
|
||||
// comment - optional JPEG comment (0/NULL if no comment), must not contain ASCII code 0xFF
|
||||
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels, unsigned short width, unsigned short height,
|
||||
bool isRGB = true, unsigned char quality = 90, bool downsample = false, const char* comment = nullptr);
|
||||
} // namespace TooJpeg
|
||||
|
||||
// My main inspiration was Jon Olick's Minimalistic JPEG writer
|
||||
// ( https://www.jonolick.com/code.html => direct link is https://www.jonolick.com/uploads/7/9/2/1/7921194/jo_jpeg.cpp ).
|
||||
// However, his code documentation is quite sparse - probably because it wasn't written from scratch and is (quote:) "based on a javascript jpeg writer",
|
||||
// most likely Andreas Ritter's code: https://github.com/eugeneware/jpeg-js/blob/master/lib/encoder.js
|
||||
//
|
||||
// Therefore I wrote the whole lib from scratch and tried hard to add tons of comments to my code, especially describing where all those magic numbers come from.
|
||||
// And I managed to remove the need for any external includes ...
|
||||
// yes, that's right: my library has no (!) includes at all, not even #include <stdlib.h>
|
||||
// Depending on your callback WRITE_ONE_BYTE, the library writes either to disk, or in-memory, or wherever you wish.
|
||||
// Moreover, no dynamic memory allocations are performed, just a few bytes on the stack.
|
||||
//
|
||||
// In contrast to Jon's code, compression can be significantly improved in many use cases:
|
||||
// a) grayscale JPEG images need just a single Y channel, no need to save the superfluous Cb + Cr channels
|
||||
// b) YCbCr 4:2:0 downsampling is often about 20% more efficient (=smaller) than the default YCbCr 4:4:4 with only little visual loss
|
||||
//
|
||||
// TooJpeg 1.2+ compresses about twice as fast as jo_jpeg (and about half as fast as libjpeg-turbo).
|
||||
// A few benchmark numbers can be found on my website https://create.stephan-brumme.com/toojpeg/#benchmark
|
||||
//
|
||||
// Last but not least you can optionally add a JPEG comment.
|
||||
//
|
||||
// Your C++ compiler needs to support a reasonable subset of C++11 (g++ 4.7 or Visual C++ 2013 are sufficient).
|
||||
// I haven't tested the code on big-endian systems or anything that smells like an apple.
|
||||
//
|
||||
// USE AT YOUR OWN RISK. Because you are a brave soul :-)
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
cmake_minimum_required(VERSION 3.12)
|
||||
project(wslay C)
|
||||
|
||||
set(WSLAY_HEADERS
|
||||
msvc_compat.h
|
||||
wslay_event.h
|
||||
wslay_frame.h
|
||||
wslay_macro.h
|
||||
wslay_net.h
|
||||
wslay_queue.h
|
||||
includes/wslay/wslay.h
|
||||
includes/wslay/wslayver.h
|
||||
)
|
||||
set(WSLAY_SOURCES
|
||||
wslay_event.c
|
||||
wslay_frame.c
|
||||
wslay_net.c
|
||||
wslay_queue.c
|
||||
)
|
||||
|
||||
add_library(wslay STATIC ${WSLAY_HEADERS} ${WSLAY_SOURCES})
|
||||
|
||||
# config.h here is checked in rather than generated, see the note in it
|
||||
target_compile_definitions(wslay PRIVATE HAVE_CONFIG_H)
|
||||
target_include_directories(wslay PRIVATE ${PROJECT_SOURCE_DIR})
|
||||
target_include_directories(wslay PUBLIC ${PROJECT_SOURCE_DIR}/includes)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(wslay PRIVATE "/FI${PROJECT_SOURCE_DIR}/msvc_compat.h")
|
||||
else()
|
||||
# upstream code, and not ours to keep warning clean
|
||||
target_compile_options(wslay PRIVATE -w)
|
||||
endif()
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/* Hand written stand-in for the autotools/CMake generated config.h. Spice only
|
||||
builds this for Windows on x86, so the probes have single known answers. */
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#define HAVE_WINSOCK2_H
|
||||
/* x86 and x86_64 are little endian, so WORDS_BIGENDIAN stays undefined */
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
+841
@@ -0,0 +1,841 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef WSLAY_H
|
||||
#define WSLAY_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
/*
|
||||
* wslay/wslayver.h is generated from wslay/wslayver.h.in by
|
||||
* configure. The projects which do not use autotools can set
|
||||
* WSLAY_VERSION macro from outside to avoid to generating wslayver.h
|
||||
*/
|
||||
#ifndef WSLAY_VERSION
|
||||
# include <wslay/wslayver.h>
|
||||
#endif /* WSLAY_VERSION */
|
||||
|
||||
enum wslay_error {
|
||||
WSLAY_ERR_WANT_READ = -100,
|
||||
WSLAY_ERR_WANT_WRITE = -101,
|
||||
WSLAY_ERR_PROTO = -200,
|
||||
WSLAY_ERR_INVALID_ARGUMENT = -300,
|
||||
WSLAY_ERR_INVALID_CALLBACK = -301,
|
||||
WSLAY_ERR_NO_MORE_MSG = -302,
|
||||
WSLAY_ERR_CALLBACK_FAILURE = -400,
|
||||
WSLAY_ERR_WOULDBLOCK = -401,
|
||||
WSLAY_ERR_NOMEM = -500
|
||||
};
|
||||
|
||||
/*
|
||||
* Status codes defined in RFC6455
|
||||
*/
|
||||
enum wslay_status_code {
|
||||
WSLAY_CODE_NORMAL_CLOSURE = 1000,
|
||||
WSLAY_CODE_GOING_AWAY = 1001,
|
||||
WSLAY_CODE_PROTOCOL_ERROR = 1002,
|
||||
WSLAY_CODE_UNSUPPORTED_DATA = 1003,
|
||||
WSLAY_CODE_NO_STATUS_RCVD = 1005,
|
||||
WSLAY_CODE_ABNORMAL_CLOSURE = 1006,
|
||||
WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA = 1007,
|
||||
WSLAY_CODE_POLICY_VIOLATION = 1008,
|
||||
WSLAY_CODE_MESSAGE_TOO_BIG = 1009,
|
||||
WSLAY_CODE_MANDATORY_EXT = 1010,
|
||||
WSLAY_CODE_INTERNAL_SERVER_ERROR = 1011,
|
||||
WSLAY_CODE_TLS_HANDSHAKE = 1015
|
||||
};
|
||||
|
||||
enum wslay_io_flags {
|
||||
/*
|
||||
* There is more data to send.
|
||||
*/
|
||||
WSLAY_MSG_MORE = 1
|
||||
};
|
||||
|
||||
/*
|
||||
* Callback function used by wslay_frame_send() function when it needs
|
||||
* to send data. The implementation of this function must send at most
|
||||
* len bytes of data in data. flags is the bitwise OR of zero or more
|
||||
* of the following flag:
|
||||
*
|
||||
* WSLAY_MSG_MORE
|
||||
* There is more data to send
|
||||
*
|
||||
* It provides some hints to tune performance and behaviour. user_data
|
||||
* is one given in wslay_frame_context_init() function. The
|
||||
* implementation of this function must return the number of bytes
|
||||
* sent. If there is an error, return -1. The return value 0 is also
|
||||
* treated an error by the library.
|
||||
*/
|
||||
typedef ssize_t (*wslay_frame_send_callback)(const uint8_t *data, size_t len,
|
||||
int flags, void *user_data);
|
||||
/*
|
||||
* Callback function used by wslay_frame_recv() function when it needs
|
||||
* more data. The implementation of this function must fill at most
|
||||
* len bytes of data into buf. The memory area of buf is allocated by
|
||||
* library and not be freed by the application code. flags is always 0
|
||||
* in this version. user_data is one given in
|
||||
* wslay_frame_context_init() function. The implementation of this
|
||||
* function must return the number of bytes filled. If there is an
|
||||
* error, return -1. The return value 0 is also treated an error by
|
||||
* the library.
|
||||
*/
|
||||
typedef ssize_t (*wslay_frame_recv_callback)(uint8_t *buf, size_t len,
|
||||
int flags, void *user_data);
|
||||
/*
|
||||
* Callback function used by wslay_frame_send() function when it needs
|
||||
* new mask key. The implementation of this function must write
|
||||
* exactly len bytes of mask key to buf. user_data is one given in
|
||||
* wslay_frame_context_init() function. The implementation of this
|
||||
* function return 0 on success. If there is an error, return -1.
|
||||
*/
|
||||
typedef int (*wslay_frame_genmask_callback)(uint8_t *buf, size_t len,
|
||||
void *user_data);
|
||||
|
||||
struct wslay_frame_callbacks {
|
||||
wslay_frame_send_callback send_callback;
|
||||
wslay_frame_recv_callback recv_callback;
|
||||
wslay_frame_genmask_callback genmask_callback;
|
||||
};
|
||||
|
||||
/*
|
||||
* The opcode defined in RFC6455.
|
||||
*/
|
||||
enum wslay_opcode {
|
||||
WSLAY_CONTINUATION_FRAME = 0x0u,
|
||||
WSLAY_TEXT_FRAME = 0x1u,
|
||||
WSLAY_BINARY_FRAME = 0x2u,
|
||||
WSLAY_CONNECTION_CLOSE = 0x8u,
|
||||
WSLAY_PING = 0x9u,
|
||||
WSLAY_PONG = 0xau
|
||||
};
|
||||
|
||||
/*
|
||||
* Macro that returns 1 if opcode is control frame opcode, otherwise
|
||||
* returns 0.
|
||||
*/
|
||||
#define wslay_is_ctrl_frame(opcode) ((opcode >> 3) & 1)
|
||||
|
||||
/*
|
||||
* Macros that represent and return reserved bits: RSV1, RSV2, RSV3.
|
||||
* These macros assume that rsv is constructed by ((RSV1 << 2) |
|
||||
* (RSV2 << 1) | RSV3)
|
||||
*/
|
||||
#define WSLAY_RSV_NONE ((uint8_t)0)
|
||||
#define WSLAY_RSV1_BIT (((uint8_t)1) << 2)
|
||||
#define WSLAY_RSV2_BIT (((uint8_t)1) << 1)
|
||||
#define WSLAY_RSV3_BIT (((uint8_t)1) << 0)
|
||||
|
||||
#define wslay_get_rsv1(rsv) ((rsv >> 2) & 1)
|
||||
#define wslay_get_rsv2(rsv) ((rsv >> 1) & 1)
|
||||
#define wslay_get_rsv3(rsv) (rsv & 1)
|
||||
|
||||
struct wslay_frame_iocb {
|
||||
/* 1 for fragmented final frame, 0 for otherwise */
|
||||
uint8_t fin;
|
||||
/*
|
||||
* reserved 3 bits. rsv = ((RSV1 << 2) | (RSV << 1) | RSV3).
|
||||
* RFC6455 requires 0 unless extensions are negotiated.
|
||||
*/
|
||||
uint8_t rsv;
|
||||
/* 4 bit opcode */
|
||||
uint8_t opcode;
|
||||
/* payload length [0, 2**63-1] */
|
||||
uint64_t payload_length;
|
||||
/* 1 for masked frame, 0 for unmasked */
|
||||
uint8_t mask;
|
||||
/* part of payload data */
|
||||
const uint8_t *data;
|
||||
/* bytes of data defined above */
|
||||
size_t data_length;
|
||||
};
|
||||
|
||||
struct wslay_frame_context;
|
||||
typedef struct wslay_frame_context *wslay_frame_context_ptr;
|
||||
|
||||
/*
|
||||
* Initializes ctx using given callbacks and user_data. This function
|
||||
* allocates memory for struct wslay_frame_context and stores the
|
||||
* result to *ctx. The callback functions specified in callbacks are
|
||||
* copied to ctx. user_data is stored in ctx and it will be passed to
|
||||
* callback functions. When the user code finished using ctx, it must
|
||||
* call wslay_frame_context_free to deallocate memory.
|
||||
*/
|
||||
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
|
||||
const struct wslay_frame_callbacks *callbacks,
|
||||
void *user_data);
|
||||
|
||||
/*
|
||||
* Deallocates memory pointed by ctx.
|
||||
*/
|
||||
void wslay_frame_context_free(wslay_frame_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Send WebSocket frame specified in iocb. ctx must be initialized
|
||||
* using wslay_frame_context_init() function. iocb->fin must be 1 if
|
||||
* this is a fin frame, otherwise 0. iocb->rsv is reserved bits.
|
||||
* iocb->opcode must be the opcode of this frame. iocb->mask must be
|
||||
* 1 if this is masked frame, otherwise 0. iocb->payload_length is
|
||||
* the payload_length of this frame. iocb->data must point to the
|
||||
* payload data to be sent. iocb->data_length must be the length of
|
||||
* the data. This function calls send_callback function if it needs
|
||||
* to send bytes. This function calls gen_mask_callback function if
|
||||
* it needs new mask key. This function returns the number of payload
|
||||
* bytes sent. Please note that it does not include any number of
|
||||
* header bytes. If it cannot send any single bytes of payload, it
|
||||
* returns WSLAY_ERR_WANT_WRITE. If the library detects error in iocb,
|
||||
* this function returns WSLAY_ERR_INVALID_ARGUMENT. If callback
|
||||
* functions report a failure, this function returns
|
||||
* WSLAY_ERR_INVALID_CALLBACK. This function does not always send all
|
||||
* given data in iocb. If there are remaining data to be sent, adjust
|
||||
* data and data_length in iocb accordingly and call this function
|
||||
* again.
|
||||
*/
|
||||
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
|
||||
struct wslay_frame_iocb *iocb);
|
||||
|
||||
/*
|
||||
* Write WebSocket frame specified in iocb to buf of length
|
||||
* buflen. ctx must be initialized using wslay_frame_context_init()
|
||||
* function. iocb->fin must be 1 if this is a fin frame, otherwise 0.
|
||||
* iocb->rsv is reserved bits. iocb->opcode must be the opcode of
|
||||
* this frame. iocb->mask must be 1 if this is masked frame,
|
||||
* otherwise 0. iocb->payload_length is the payload_length of this
|
||||
* frame. iocb->data must point to the payload data to be
|
||||
* sent. iocb->data_length must be the length of the data. Unlike
|
||||
* wslay_frame_send, this function does not call send_callback
|
||||
* function. This function calls gen_mask_callback function if it
|
||||
* needs new mask key. This function returns the number of bytes
|
||||
* written to a buffer. Unlike wslay_frame_send, it includes the
|
||||
* number of header bytes. Instead, the number of payload bytes
|
||||
* written is assigned to *pwpayloadlen if this function succeeds. If
|
||||
* there is not enough space left in a buffer, it returns 0. If the
|
||||
* library detects error in iocb, this function returns
|
||||
* WSLAY_ERR_INVALID_ARGUMENT. If callback functions report a
|
||||
* failure, this function returns WSLAY_ERR_INVALID_CALLBACK. This
|
||||
* function does not always send all given data in iocb. If there are
|
||||
* remaining data to be sent, adjust data and data_length in iocb
|
||||
* accordingly and call this function again.
|
||||
*/
|
||||
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
|
||||
struct wslay_frame_iocb *iocb, uint8_t *buf,
|
||||
size_t buflen, size_t *pwpayloadlen);
|
||||
|
||||
/*
|
||||
* Receives WebSocket frame and stores it in iocb. This function
|
||||
* returns the number of payload bytes received. This does not
|
||||
* include header bytes. In this case, iocb will be populated as
|
||||
* follows: iocb->fin is 1 if received frame is fin frame, otherwise
|
||||
* 0. iocb->rsv is reserved bits of received frame. iocb->opcode is
|
||||
* opcode of received frame. iocb->mask is 1 if received frame is
|
||||
* masked, otherwise 0. iocb->payload_length is the payload length of
|
||||
* received frame. iocb->data is pointed to the buffer containing
|
||||
* received payload data. This buffer is allocated by the library and
|
||||
* must be read-only. iocb->data_length is the number of payload
|
||||
* bytes recieved. This function calls recv_callback if it needs to
|
||||
* receive additional bytes. If it cannot receive any single bytes of
|
||||
* payload, it returns WSLAY_ERR_WANT_READ. If the library detects
|
||||
* protocol violation in a received frame, this function returns
|
||||
* WSLAY_ERR_PROTO. If callback functions report a failure, this
|
||||
* function returns WSLAY_ERR_INVALID_CALLBACK. This function does
|
||||
* not always receive whole frame in a single call. If there are
|
||||
* remaining data to be received, call this function again. This
|
||||
* function ensures frame alignment.
|
||||
*/
|
||||
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
|
||||
struct wslay_frame_iocb *iocb);
|
||||
|
||||
struct wslay_event_context;
|
||||
/* Pointer to the event-based API context */
|
||||
typedef struct wslay_event_context *wslay_event_context_ptr;
|
||||
|
||||
struct wslay_event_on_msg_recv_arg {
|
||||
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
|
||||
uint8_t rsv;
|
||||
/* opcode */
|
||||
uint8_t opcode;
|
||||
/* received message */
|
||||
const uint8_t *msg;
|
||||
/* message length */
|
||||
size_t msg_length;
|
||||
/*
|
||||
* Status code iff opcode == WSLAY_CONNECTION_CLOSE. If no status
|
||||
* code is included in the close control frame, it is set to 0.
|
||||
*/
|
||||
uint16_t status_code;
|
||||
};
|
||||
|
||||
/*
|
||||
* Callback function invoked by wslay_event_recv() when a message is
|
||||
* completely received.
|
||||
*/
|
||||
typedef void (*wslay_event_on_msg_recv_callback)(
|
||||
wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg,
|
||||
void *user_data);
|
||||
|
||||
struct wslay_event_on_frame_recv_start_arg {
|
||||
/* fin bit; 1 for final frame, or 0. */
|
||||
uint8_t fin;
|
||||
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
|
||||
uint8_t rsv;
|
||||
/* opcode of the frame */
|
||||
uint8_t opcode;
|
||||
/* payload length of ths frame */
|
||||
uint64_t payload_length;
|
||||
};
|
||||
|
||||
/*
|
||||
* Callback function invoked by wslay_event_recv() when a new frame
|
||||
* starts to be received. This callback function is only invoked once
|
||||
* for each frame.
|
||||
*/
|
||||
typedef void (*wslay_event_on_frame_recv_start_callback)(
|
||||
wslay_event_context_ptr ctx,
|
||||
const struct wslay_event_on_frame_recv_start_arg *arg, void *user_data);
|
||||
|
||||
struct wslay_event_on_frame_recv_chunk_arg {
|
||||
/* chunk of payload data */
|
||||
const uint8_t *data;
|
||||
/* length of data */
|
||||
size_t data_length;
|
||||
};
|
||||
|
||||
/*
|
||||
* Callback function invoked by wslay_event_recv() when a chunk of
|
||||
* frame payload is received.
|
||||
*/
|
||||
typedef void (*wslay_event_on_frame_recv_chunk_callback)(
|
||||
wslay_event_context_ptr ctx,
|
||||
const struct wslay_event_on_frame_recv_chunk_arg *arg, void *user_data);
|
||||
|
||||
/*
|
||||
* Callback function invoked by wslay_event_recv() when a frame is
|
||||
* completely received.
|
||||
*/
|
||||
typedef void (*wslay_event_on_frame_recv_end_callback)(
|
||||
wslay_event_context_ptr ctx, void *user_data);
|
||||
|
||||
/*
|
||||
* Callback function invoked by wslay_event_recv() when it wants to
|
||||
* receive more data from peer. The implementation of this callback
|
||||
* function must read data at most len bytes from peer and store them
|
||||
* in buf and return the number of bytes read. flags is always 0 in
|
||||
* this version.
|
||||
*
|
||||
* If there is an error, return -1 and set error code
|
||||
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
|
||||
* event-based API on the whole assumes non-blocking I/O. If the cause
|
||||
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
|
||||
* instead. This is important because it tells wslay_event_recv() to
|
||||
* stop receiving further data and return.
|
||||
*/
|
||||
typedef ssize_t (*wslay_event_recv_callback)(wslay_event_context_ptr ctx,
|
||||
uint8_t *buf, size_t len,
|
||||
int flags, void *user_data);
|
||||
|
||||
/*
|
||||
* Callback function invoked by wslay_event_send() when it wants to
|
||||
* send more data to peer. The implementation of this callback
|
||||
* function must send data at most len bytes to peer and return the
|
||||
* number of bytes sent. flags is the bitwise OR of zero or more of
|
||||
* the following flag:
|
||||
*
|
||||
* WSLAY_MSG_MORE
|
||||
* There is more data to send
|
||||
*
|
||||
* It provides some hints to tune performance and behaviour.
|
||||
*
|
||||
* If there is an error, return -1 and set error code
|
||||
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
|
||||
* event-based API on the whole assumes non-blocking I/O. If the cause
|
||||
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
|
||||
* instead. This is important because it tells wslay_event_send() to
|
||||
* stop sending data and return.
|
||||
*/
|
||||
typedef ssize_t (*wslay_event_send_callback)(wslay_event_context_ptr ctx,
|
||||
const uint8_t *data, size_t len,
|
||||
int flags, void *user_data);
|
||||
|
||||
/*
|
||||
* Callback function invoked by wslay_event_send() when it wants new
|
||||
* mask key. As described in RFC6455, only the traffic from WebSocket
|
||||
* client is masked, so this callback function is only needed if an
|
||||
* event-based API is initialized for WebSocket client use.
|
||||
*/
|
||||
typedef int (*wslay_event_genmask_callback)(wslay_event_context_ptr ctx,
|
||||
uint8_t *buf, size_t len,
|
||||
void *user_data);
|
||||
|
||||
struct wslay_event_callbacks {
|
||||
wslay_event_recv_callback recv_callback;
|
||||
wslay_event_send_callback send_callback;
|
||||
wslay_event_genmask_callback genmask_callback;
|
||||
wslay_event_on_frame_recv_start_callback on_frame_recv_start_callback;
|
||||
wslay_event_on_frame_recv_chunk_callback on_frame_recv_chunk_callback;
|
||||
wslay_event_on_frame_recv_end_callback on_frame_recv_end_callback;
|
||||
wslay_event_on_msg_recv_callback on_msg_recv_callback;
|
||||
};
|
||||
|
||||
/*
|
||||
* Initializes ctx as WebSocket Server. user_data is an arbitrary
|
||||
* pointer, which is directly passed to each callback functions as
|
||||
* user_data argument.
|
||||
*
|
||||
* On success, returns 0. On error, returns one of following negative
|
||||
* values:
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*/
|
||||
int wslay_event_context_server_init(
|
||||
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
|
||||
void *user_data);
|
||||
|
||||
/*
|
||||
* Initializes ctx as WebSocket client. user_data is an arbitrary
|
||||
* pointer, which is directly passed to each callback functions as
|
||||
* user_data argument.
|
||||
*
|
||||
* On success, returns 0. On error, returns one of following negative
|
||||
* values:
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*/
|
||||
int wslay_event_context_client_init(
|
||||
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
|
||||
void *user_data);
|
||||
|
||||
/*
|
||||
* Releases allocated resources for ctx.
|
||||
*/
|
||||
void wslay_event_context_free(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Sets a bit mask of allowed reserved bits.
|
||||
* Currently only permitted values are WSLAY_RSV1_BIT to allow PMCE
|
||||
* extension (see RFC-7692) or WSLAY_RSV_NONE to disable.
|
||||
*
|
||||
* Default: WSLAY_RSV_NONE
|
||||
*/
|
||||
void wslay_event_config_set_allowed_rsv_bits(wslay_event_context_ptr ctx,
|
||||
uint8_t rsv);
|
||||
|
||||
/*
|
||||
* Enables or disables buffering of an entire message for non-control
|
||||
* frames. If val is 0, buffering is enabled. Otherwise, buffering is
|
||||
* disabled. If wslay_event_on_msg_recv_callback is invoked when
|
||||
* buffering is disabled, the msg_length member of struct
|
||||
* wslay_event_on_msg_recv_arg is set to 0.
|
||||
*
|
||||
* The control frames are always buffered regardless of this function call.
|
||||
*
|
||||
* This function must not be used after the first invocation of
|
||||
* wslay_event_recv() function.
|
||||
*/
|
||||
void wslay_event_config_set_no_buffering(wslay_event_context_ptr ctx, int val);
|
||||
|
||||
/*
|
||||
* Sets maximum length of a message that can be received. The length
|
||||
* of message is checked by wslay_event_recv() function. If the length
|
||||
* of a message is larger than this value, reading operation is
|
||||
* disabled (same effect with wslay_event_shutdown_read() call) and
|
||||
* close control frame with WSLAY_CODE_MESSAGE_TOO_BIG is queued. If
|
||||
* buffering for non-control frames is disabled, the library checks
|
||||
* each frame payload length and does not check length of entire
|
||||
* message.
|
||||
*
|
||||
* The default value is (1u << 31)-1.
|
||||
*/
|
||||
void wslay_event_config_set_max_recv_msg_length(wslay_event_context_ptr ctx,
|
||||
uint64_t val);
|
||||
|
||||
/*
|
||||
* Sets callbacks to ctx. The callbacks previously set by this function
|
||||
* or wslay_event_context_server_init() or
|
||||
* wslay_event_context_client_init() are replaced with callbacks.
|
||||
*/
|
||||
void wslay_event_config_set_callbacks(
|
||||
wslay_event_context_ptr ctx, const struct wslay_event_callbacks *callbacks);
|
||||
|
||||
/*
|
||||
* Receives messages from peer. When receiving
|
||||
* messages, it uses wslay_event_recv_callback function. Single call
|
||||
* of this function receives multiple messages until
|
||||
* wslay_event_recv_callback function sets error code
|
||||
* WSLAY_ERR_WOULDBLOCK.
|
||||
*
|
||||
* When close control frame is received, this function automatically
|
||||
* queues close control frame. Also this function calls
|
||||
* wslay_event_set_read_enabled() with second argument 0 to disable
|
||||
* further read from peer.
|
||||
*
|
||||
* When ping control frame is received, this function automatically
|
||||
* queues pong control frame.
|
||||
*
|
||||
* In case of a fatal errror which leads to negative return code, this
|
||||
* function calls wslay_event_set_read_enabled() with second argument
|
||||
* 0 to disable further read from peer.
|
||||
*
|
||||
* wslay_event_recv() returns 0 if it succeeds, or one of the
|
||||
* following negative error codes:
|
||||
*
|
||||
* WSLAY_ERR_CALLBACK_FAILURE
|
||||
* User defined callback function is failed.
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*
|
||||
* When negative error code is returned, application must not make any
|
||||
* further call of wslay_event_recv() and must close WebSocket
|
||||
* connection.
|
||||
*/
|
||||
int wslay_event_recv(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Sends queued messages to peer. When sending a
|
||||
* message, it uses wslay_event_send_callback function. Single call of
|
||||
* wslay_event_send() sends multiple messages until
|
||||
* wslay_event_send_callback sets error code WSLAY_ERR_WOULDBLOCK.
|
||||
*
|
||||
* If ctx is initialized for WebSocket client use, wslay_event_send()
|
||||
* uses wslay_event_genmask_callback to get new mask key.
|
||||
*
|
||||
* When a message queued using wslay_event_queue_fragmented_msg() is
|
||||
* sent, wslay_event_send() invokes
|
||||
* wslay_event_fragmented_msg_callback for that message.
|
||||
*
|
||||
* After close control frame is sent, this function calls
|
||||
* wslay_event_set_write_enabled() with second argument 0 to disable
|
||||
* further transmission to peer.
|
||||
*
|
||||
* If there are any pending messages, wslay_event_want_write() returns
|
||||
* 1, otherwise returns 0.
|
||||
*
|
||||
* In case of a fatal errror which leads to negative return code, this
|
||||
* function calls wslay_event_set_write_enabled() with second argument
|
||||
* 0 to disable further transmission to peer.
|
||||
*
|
||||
* wslay_event_send() returns 0 if it succeeds, or one of the
|
||||
* following negative error codes:
|
||||
*
|
||||
* WSLAY_ERR_CALLBACK_FAILURE
|
||||
* User defined callback function is failed.
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*
|
||||
* When negative error code is returned, application must not make any
|
||||
* further call of wslay_event_send() and must close WebSocket
|
||||
* connection.
|
||||
*/
|
||||
int wslay_event_send(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Writes queued messages to a buffer. Unlike wslay_event_send(), this
|
||||
* function writes messages into the given buffer. It does not use
|
||||
* wslay_event_send_callback function. Single call of
|
||||
* wslay_event_write() writes multiple messages until there is not
|
||||
* enough space left in a buffer.
|
||||
*
|
||||
* If ctx is initialized for WebSocket client use, wslay_event_write()
|
||||
* uses wslay_event_genmask_callback to get new mask key.
|
||||
*
|
||||
* buf is a pointer to buffer and its capacity is given in buflen. It
|
||||
* should have at least 14 bytes.
|
||||
*
|
||||
* When a message queued using wslay_event_queue_fragmented_msg() is
|
||||
* sent, wslay_event_write() invokes
|
||||
* wslay_event_fragmented_msg_callback for that message.
|
||||
*
|
||||
* After close control frame is sent, this function calls
|
||||
* wslay_event_set_write_enabled() with second argument 0 to disable
|
||||
* further transmission to peer.
|
||||
*
|
||||
* If there are any pending messages, wslay_event_want_write() returns
|
||||
* 1, otherwise returns 0.
|
||||
*
|
||||
* In case of a fatal errror which leads to negative return code, this
|
||||
* function calls wslay_event_set_write_enabled() with second argument
|
||||
* 0 to disable further transmission to peer.
|
||||
*
|
||||
* wslay_event_write() returns the number of bytes written to a buffer
|
||||
* if it succeeds, or one of the following negative error codes:
|
||||
*
|
||||
* WSLAY_ERR_CALLBACK_FAILURE
|
||||
* User defined callback function is failed.
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*
|
||||
* When negative error code is returned, application must not make any
|
||||
* further call of wslay_event_write() and must close WebSocket
|
||||
* connection.
|
||||
*/
|
||||
ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf,
|
||||
size_t buflen);
|
||||
|
||||
struct wslay_event_msg {
|
||||
uint8_t opcode;
|
||||
const uint8_t *msg;
|
||||
size_t msg_length;
|
||||
};
|
||||
|
||||
/*
|
||||
* Queues message specified in arg.
|
||||
*
|
||||
* This function supports both control and non-control messages and
|
||||
* the given message is sent without fragmentation. If fragmentation
|
||||
* is needed, use wslay_event_queue_fragmented_msg() function instead.
|
||||
*
|
||||
* This function just queues a message and does not send
|
||||
* it. wslay_event_send() function call sends these queued messages.
|
||||
*
|
||||
* wslay_event_queue_msg() returns 0 if it succeeds, or returns the
|
||||
* following negative error codes:
|
||||
*
|
||||
* WSLAY_ERR_NO_MORE_MSG
|
||||
* Could not queue given message. The one of possible reason is that
|
||||
* close control frame has been queued/sent and no further queueing
|
||||
* message is not allowed.
|
||||
*
|
||||
* WSLAY_ERR_INVALID_ARGUMENT
|
||||
* The given message is invalid.
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*/
|
||||
int wslay_event_queue_msg(wslay_event_context_ptr ctx,
|
||||
const struct wslay_event_msg *arg);
|
||||
|
||||
/*
|
||||
* Extended version of wslay_event_queue_msg which allows to set reserved bits.
|
||||
*/
|
||||
int wslay_event_queue_msg_ex(wslay_event_context_ptr ctx,
|
||||
const struct wslay_event_msg *arg, uint8_t rsv);
|
||||
|
||||
/*
|
||||
* Specify "source" to generate message.
|
||||
*/
|
||||
union wslay_event_msg_source {
|
||||
int fd;
|
||||
void *data;
|
||||
};
|
||||
|
||||
/*
|
||||
* Callback function called by wslay_event_send() to read message data
|
||||
* from source. The implementation of
|
||||
* wslay_event_fragmented_msg_callback must store at most len bytes of
|
||||
* data to buf and return the number of stored bytes. If all data is
|
||||
* read (i.e., EOF), set *eof to 1. If no data can be generated at the
|
||||
* moment, return 0. If there is an error, return -1 and set error
|
||||
* code WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error().
|
||||
*/
|
||||
typedef ssize_t (*wslay_event_fragmented_msg_callback)(
|
||||
wslay_event_context_ptr ctx, uint8_t *buf, size_t len,
|
||||
const union wslay_event_msg_source *source, int *eof, void *user_data);
|
||||
|
||||
struct wslay_event_fragmented_msg {
|
||||
/* opcode */
|
||||
uint8_t opcode;
|
||||
/* "source" to generate message data */
|
||||
union wslay_event_msg_source source;
|
||||
/* Callback function to read message data from source. */
|
||||
wslay_event_fragmented_msg_callback read_callback;
|
||||
};
|
||||
|
||||
/*
|
||||
* Queues a fragmented message specified in arg.
|
||||
*
|
||||
* This function supports non-control messages only. For control frames,
|
||||
* use wslay_event_queue_msg() or wslay_event_queue_close().
|
||||
*
|
||||
* This function just queues a message and does not send
|
||||
* it. wslay_event_send() function call sends these queued messages.
|
||||
*
|
||||
* wslay_event_queue_fragmented_msg() returns 0 if it succeeds, or
|
||||
* returns the following negative error codes:
|
||||
*
|
||||
* WSLAY_ERR_NO_MORE_MSG
|
||||
* Could not queue given message. The one of possible reason is that
|
||||
* close control frame has been queued/sent and no further queueing
|
||||
* message is not allowed.
|
||||
*
|
||||
* WSLAY_ERR_INVALID_ARGUMENT
|
||||
* The given message is invalid.
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*/
|
||||
int wslay_event_queue_fragmented_msg(
|
||||
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg);
|
||||
|
||||
/*
|
||||
* Extended version of wslay_event_queue_fragmented_msg which allows to set
|
||||
* reserved bits.
|
||||
*/
|
||||
int wslay_event_queue_fragmented_msg_ex(
|
||||
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg,
|
||||
uint8_t rsv);
|
||||
|
||||
/*
|
||||
* Queues close control frame. This function is provided just for
|
||||
* convenience. wslay_event_queue_msg() can queue a close control
|
||||
* frame as well. status_code is the status code of close control
|
||||
* frame. reason is the close reason encoded in UTF-8. reason_length
|
||||
* is the length of reason in bytes. reason_length must be less than
|
||||
* 123 bytes.
|
||||
*
|
||||
* If status_code is 0, reason and reason_length is not used and close
|
||||
* control frame with zero-length payload will be queued.
|
||||
*
|
||||
* This function just queues a message and does not send
|
||||
* it. wslay_event_send() function call sends these queued messages.
|
||||
*
|
||||
* wslay_event_queue_close() returns 0 if it succeeds, or returns the
|
||||
* following negative error codes:
|
||||
*
|
||||
* WSLAY_ERR_NO_MORE_MSG
|
||||
* Could not queue given message. The one of possible reason is that
|
||||
* close control frame has been queued/sent and no further queueing
|
||||
* message is not allowed.
|
||||
*
|
||||
* WSLAY_ERR_INVALID_ARGUMENT
|
||||
* The given message is invalid.
|
||||
*
|
||||
* WSLAY_ERR_NOMEM
|
||||
* Out of memory.
|
||||
*/
|
||||
int wslay_event_queue_close(wslay_event_context_ptr ctx, uint16_t status_code,
|
||||
const uint8_t *reason, size_t reason_length);
|
||||
|
||||
/*
|
||||
* Sets error code to tell the library there is an error. This
|
||||
* function is typically used in user defined callback functions. See
|
||||
* the description of callback function to know which error code
|
||||
* should be used.
|
||||
*/
|
||||
void wslay_event_set_error(wslay_event_context_ptr ctx, int val);
|
||||
|
||||
/*
|
||||
* Query whehter the library want to read more data from peer.
|
||||
*
|
||||
* wslay_event_want_read() returns 1 if the library want to read more
|
||||
* data from peer, or returns 0.
|
||||
*/
|
||||
int wslay_event_want_read(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Query whehter the library want to send more data to peer.
|
||||
*
|
||||
* wslay_event_want_write() returns 1 if the library want to send more
|
||||
* data to peer, or returns 0.
|
||||
*/
|
||||
int wslay_event_want_write(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Prevents the event-based API context from reading any further data
|
||||
* from peer.
|
||||
*
|
||||
* This function may be used with wslay_event_queue_close() if the
|
||||
* application detects error in the data received and wants to fail
|
||||
* WebSocket connection.
|
||||
*/
|
||||
void wslay_event_shutdown_read(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Prevents the event-based API context from sending any further data
|
||||
* to peer.
|
||||
*/
|
||||
void wslay_event_shutdown_write(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns 1 if the event-based API context allows read operation, or
|
||||
* return 0.
|
||||
*
|
||||
* After wslay_event_shutdown_read() is called,
|
||||
* wslay_event_get_read_enabled() returns 0.
|
||||
*/
|
||||
int wslay_event_get_read_enabled(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns 1 if the event-based API context allows write operation, or
|
||||
* return 0.
|
||||
*
|
||||
* After wslay_event_shutdown_write() is called,
|
||||
* wslay_event_get_write_enabled() returns 0.
|
||||
*/
|
||||
int wslay_event_get_write_enabled(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns 1 if a close control frame has been received from peer, or
|
||||
* returns 0.
|
||||
*/
|
||||
int wslay_event_get_close_received(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns 1 if a close control frame has been sent to peer, or
|
||||
* returns 0.
|
||||
*/
|
||||
int wslay_event_get_close_sent(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns status code received in close control frame. If no close
|
||||
* control frame has not been received, returns
|
||||
* WSLAY_CODE_ABNORMAL_CLOSURE. If received close control frame has no
|
||||
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
|
||||
*/
|
||||
uint16_t wslay_event_get_status_code_received(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns status code sent in close control frame. If no close
|
||||
* control frame has not been sent, returns
|
||||
* WSLAY_CODE_ABNORMAL_CLOSURE. If sent close control frame has no
|
||||
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
|
||||
*/
|
||||
uint16_t wslay_event_get_status_code_sent(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns the number of queued messages.
|
||||
*/
|
||||
size_t wslay_event_get_queued_msg_count(wslay_event_context_ptr ctx);
|
||||
|
||||
/*
|
||||
* Returns the sum of queued message length. It only counts the
|
||||
* message length queued using wslay_event_queue_msg() or
|
||||
* wslay_event_queue_close().
|
||||
*/
|
||||
size_t wslay_event_get_queued_msg_length(wslay_event_context_ptr ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* WSLAY_H */
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef WSLAYVER_H
|
||||
#define WSLAYVER_H
|
||||
|
||||
/* normally substituted by the build system; pinned to the vendored release */
|
||||
#define WSLAY_VERSION "1.1.1"
|
||||
|
||||
#endif /* WSLAYVER_H */
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <BaseTsd.h>
|
||||
#ifndef _SSIZE_T_DEFINED
|
||||
#define _SSIZE_T_DEFINED
|
||||
typedef SSIZE_T ssize_t;
|
||||
#endif
|
||||
#endif
|
||||
+1082
File diff suppressed because it is too large
Load Diff
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef WSLAY_EVENT_H
|
||||
#define WSLAY_EVENT_H
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif /* HAVE_CONFIG_H */
|
||||
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
#include "wslay_queue.h"
|
||||
|
||||
struct wslay_event_byte_chunk {
|
||||
struct wslay_queue_entry qe;
|
||||
uint8_t *data;
|
||||
size_t data_length;
|
||||
};
|
||||
|
||||
struct wslay_event_imsg {
|
||||
uint8_t fin;
|
||||
uint8_t rsv;
|
||||
uint8_t opcode;
|
||||
uint32_t utf8state;
|
||||
struct wslay_queue chunks;
|
||||
size_t msg_length;
|
||||
};
|
||||
|
||||
enum wslay_event_msg_type { WSLAY_NON_FRAGMENTED, WSLAY_FRAGMENTED };
|
||||
|
||||
struct wslay_event_omsg {
|
||||
struct wslay_queue_entry qe;
|
||||
uint8_t fin;
|
||||
uint8_t opcode;
|
||||
uint8_t rsv;
|
||||
enum wslay_event_msg_type type;
|
||||
|
||||
uint8_t *data;
|
||||
size_t data_length;
|
||||
|
||||
union wslay_event_msg_source source;
|
||||
wslay_event_fragmented_msg_callback read_callback;
|
||||
};
|
||||
|
||||
struct wslay_event_frame_user_data {
|
||||
wslay_event_context_ptr ctx;
|
||||
void *user_data;
|
||||
};
|
||||
|
||||
enum wslay_event_close_status {
|
||||
WSLAY_CLOSE_RECEIVED = 1 << 0,
|
||||
WSLAY_CLOSE_QUEUED = 1 << 1,
|
||||
WSLAY_CLOSE_SENT = 1 << 2
|
||||
};
|
||||
|
||||
enum wslay_event_config { WSLAY_CONFIG_NO_BUFFERING = 1 << 0 };
|
||||
|
||||
struct wslay_event_context {
|
||||
/* config status, bitwise OR of enum wslay_event_config values*/
|
||||
uint32_t config;
|
||||
/* maximum message length that can be received */
|
||||
uint64_t max_recv_msg_length;
|
||||
/* 1 if initialized for server, otherwise 0 */
|
||||
uint8_t server;
|
||||
/* bitwise OR of enum wslay_event_close_status values */
|
||||
uint8_t close_status;
|
||||
/* status code in received close control frame */
|
||||
uint16_t status_code_recv;
|
||||
/* status code in sent close control frame */
|
||||
uint16_t status_code_sent;
|
||||
wslay_frame_context_ptr frame_ctx;
|
||||
/* 1 if reading is enabled, otherwise 0. Upon receiving close
|
||||
control frame this value set to 0. If any errors in read
|
||||
operation will also set this value to 0. */
|
||||
uint8_t read_enabled;
|
||||
/* 1 if writing is enabled, otherwise 0 Upon completing sending
|
||||
close control frame, this value set to 0. If any errors in write
|
||||
opration will also set this value to 0. */
|
||||
uint8_t write_enabled;
|
||||
/* imsg buffer to allow interleaved control frame between
|
||||
non-control frames. */
|
||||
struct wslay_event_imsg imsgs[2];
|
||||
/* Pointer to imsgs to indicate current used buffer. */
|
||||
struct wslay_event_imsg *imsg;
|
||||
/* payload length of frame currently being received. */
|
||||
uint64_t ipayloadlen;
|
||||
/* next byte offset of payload currently being received. */
|
||||
uint64_t ipayloadoff;
|
||||
/* error value set by user callback */
|
||||
int error;
|
||||
/* Pointer to the message currently being sent. NULL if no message
|
||||
is currently sent. */
|
||||
struct wslay_event_omsg *omsg;
|
||||
/* Queue for non-control frames */
|
||||
struct wslay_queue /*<wslay_omsg*>*/ send_queue;
|
||||
/* Queue for control frames */
|
||||
struct wslay_queue /*<wslay_omsg*>*/ send_ctrl_queue;
|
||||
/* Size of send_queue + size of send_ctrl_queue */
|
||||
size_t queued_msg_count;
|
||||
/* The sum of message length in send_queue */
|
||||
size_t queued_msg_length;
|
||||
/* Buffer used for fragmented messages */
|
||||
uint8_t obuf[4096];
|
||||
uint8_t *obuflimit;
|
||||
uint8_t *obufmark;
|
||||
/* payload length of frame currently being sent. */
|
||||
uint64_t opayloadlen;
|
||||
/* next byte offset of payload currently being sent. */
|
||||
uint64_t opayloadoff;
|
||||
struct wslay_event_callbacks callbacks;
|
||||
struct wslay_event_frame_user_data frame_user_data;
|
||||
void *user_data;
|
||||
uint8_t allowed_rsv_bits;
|
||||
};
|
||||
|
||||
#endif /* WSLAY_EVENT_H */
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#include "wslay_frame.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "wslay_net.h"
|
||||
|
||||
#define wslay_min(A, B) (((A) < (B)) ? (A) : (B))
|
||||
|
||||
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
|
||||
const struct wslay_frame_callbacks *callbacks,
|
||||
void *user_data) {
|
||||
*ctx = malloc(sizeof(struct wslay_frame_context));
|
||||
if (*ctx == NULL) {
|
||||
return -1;
|
||||
}
|
||||
memset(*ctx, 0, sizeof(struct wslay_frame_context));
|
||||
(*ctx)->istate = RECV_HEADER1;
|
||||
(*ctx)->ireqread = 2;
|
||||
(*ctx)->ostate = PREP_HEADER;
|
||||
(*ctx)->user_data = user_data;
|
||||
(*ctx)->ibufmark = (*ctx)->ibuflimit = (*ctx)->ibuf;
|
||||
(*ctx)->callbacks = *callbacks;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void wslay_frame_context_free(wslay_frame_context_ptr ctx) { free(ctx); }
|
||||
|
||||
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
|
||||
struct wslay_frame_iocb *iocb) {
|
||||
if (iocb->data_length > iocb->payload_length) {
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (ctx->ostate == PREP_HEADER) {
|
||||
uint8_t *hdptr = ctx->oheader;
|
||||
memset(ctx->oheader, 0, sizeof(ctx->oheader));
|
||||
*hdptr |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
|
||||
*hdptr |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
|
||||
/* Suppress stubborn gcc-10 warning */
|
||||
*hdptr |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
|
||||
++hdptr;
|
||||
*hdptr |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
|
||||
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (iocb->payload_length < 126) {
|
||||
*hdptr |= (uint8_t)iocb->payload_length;
|
||||
++hdptr;
|
||||
} else if (iocb->payload_length < (1 << 16)) {
|
||||
uint16_t len = htons((uint16_t)iocb->payload_length);
|
||||
*hdptr |= 126;
|
||||
++hdptr;
|
||||
memcpy(hdptr, &len, 2);
|
||||
hdptr += 2;
|
||||
} else if (iocb->payload_length < (1ull << 63)) {
|
||||
uint64_t len = hton64(iocb->payload_length);
|
||||
*hdptr |= 127;
|
||||
++hdptr;
|
||||
memcpy(hdptr, &len, 8);
|
||||
hdptr += 8;
|
||||
} else {
|
||||
/* Too large payload length */
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (iocb->mask) {
|
||||
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
|
||||
0) {
|
||||
return WSLAY_ERR_INVALID_CALLBACK;
|
||||
} else {
|
||||
ctx->omask = 1;
|
||||
memcpy(hdptr, ctx->omaskkey, 4);
|
||||
hdptr += 4;
|
||||
}
|
||||
}
|
||||
ctx->ostate = SEND_HEADER;
|
||||
ctx->oheadermark = ctx->oheader;
|
||||
ctx->oheaderlimit = hdptr;
|
||||
ctx->opayloadlen = iocb->payload_length;
|
||||
ctx->opayloadoff = 0;
|
||||
}
|
||||
if (ctx->ostate == SEND_HEADER) {
|
||||
ptrdiff_t len = ctx->oheaderlimit - ctx->oheadermark;
|
||||
ssize_t r;
|
||||
int flags = 0;
|
||||
if (iocb->data_length > 0) {
|
||||
flags |= WSLAY_MSG_MORE;
|
||||
}
|
||||
r = ctx->callbacks.send_callback(ctx->oheadermark, (size_t)len, flags,
|
||||
ctx->user_data);
|
||||
if (r > 0) {
|
||||
if (r > len) {
|
||||
return WSLAY_ERR_INVALID_CALLBACK;
|
||||
} else {
|
||||
ctx->oheadermark += r;
|
||||
if (ctx->oheadermark == ctx->oheaderlimit) {
|
||||
ctx->ostate = SEND_PAYLOAD;
|
||||
} else {
|
||||
return WSLAY_ERR_WANT_WRITE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return WSLAY_ERR_WANT_WRITE;
|
||||
}
|
||||
}
|
||||
if (ctx->ostate == SEND_PAYLOAD) {
|
||||
size_t totallen = 0;
|
||||
if (iocb->data_length > 0) {
|
||||
if (ctx->omask) {
|
||||
uint8_t temp[4096];
|
||||
const uint8_t *datamark = iocb->data,
|
||||
*datalimit = iocb->data + iocb->data_length;
|
||||
while (datamark < datalimit) {
|
||||
size_t datalen = (size_t)(datalimit - datamark);
|
||||
const uint8_t *writelimit =
|
||||
datamark + wslay_min(sizeof(temp), datalen);
|
||||
size_t writelen = (size_t)(writelimit - datamark);
|
||||
ssize_t r;
|
||||
size_t i;
|
||||
for (i = 0; i < writelen; ++i) {
|
||||
temp[i] = datamark[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
|
||||
}
|
||||
r = ctx->callbacks.send_callback(temp, writelen, 0, ctx->user_data);
|
||||
if (r > 0) {
|
||||
if ((size_t)r > writelen) {
|
||||
return WSLAY_ERR_INVALID_CALLBACK;
|
||||
} else {
|
||||
datamark += r;
|
||||
ctx->opayloadoff += (uint64_t)r;
|
||||
totallen += (size_t)r;
|
||||
}
|
||||
} else {
|
||||
if (totallen > 0) {
|
||||
break;
|
||||
} else {
|
||||
return WSLAY_ERR_WANT_WRITE;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ssize_t r;
|
||||
r = ctx->callbacks.send_callback(iocb->data, iocb->data_length, 0,
|
||||
ctx->user_data);
|
||||
if (r > 0) {
|
||||
if ((size_t)r > iocb->data_length) {
|
||||
return WSLAY_ERR_INVALID_CALLBACK;
|
||||
} else {
|
||||
ctx->opayloadoff += (uint64_t)r;
|
||||
totallen = (size_t)r;
|
||||
}
|
||||
} else {
|
||||
return WSLAY_ERR_WANT_WRITE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx->opayloadoff == ctx->opayloadlen) {
|
||||
ctx->ostate = PREP_HEADER;
|
||||
}
|
||||
return (ssize_t)totallen;
|
||||
}
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
|
||||
struct wslay_frame_iocb *iocb, uint8_t *buf,
|
||||
size_t buflen, size_t *pwpayloadlen) {
|
||||
uint8_t *buf_last = buf;
|
||||
size_t i;
|
||||
size_t hdlen;
|
||||
|
||||
*pwpayloadlen = 0;
|
||||
|
||||
if (iocb->data_length > iocb->payload_length) {
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
switch (ctx->ostate) {
|
||||
case PREP_HEADER:
|
||||
case PREP_HEADER_NOBUF:
|
||||
hdlen = 2;
|
||||
if (iocb->payload_length < 126) {
|
||||
/* nothing to do */
|
||||
} else if (iocb->payload_length < (1 << 16)) {
|
||||
hdlen += 2;
|
||||
} else if (iocb->payload_length < (1ull << 63)) {
|
||||
hdlen += 8;
|
||||
}
|
||||
if (iocb->mask) {
|
||||
hdlen += 4;
|
||||
}
|
||||
|
||||
if (buflen < hdlen) {
|
||||
ctx->ostate = PREP_HEADER_NOBUF;
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(buf_last, 0, hdlen);
|
||||
*buf_last |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
|
||||
*buf_last |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
|
||||
/* Suppress stubborn gcc-10 warning */
|
||||
*buf_last |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
|
||||
++buf_last;
|
||||
*buf_last |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
|
||||
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (iocb->payload_length < 126) {
|
||||
*buf_last |= (uint8_t)iocb->payload_length;
|
||||
++buf_last;
|
||||
} else if (iocb->payload_length < (1 << 16)) {
|
||||
uint16_t len = htons((uint16_t)iocb->payload_length);
|
||||
*buf_last |= 126;
|
||||
++buf_last;
|
||||
memcpy(buf_last, &len, 2);
|
||||
buf_last += 2;
|
||||
} else if (iocb->payload_length < (1ull << 63)) {
|
||||
uint64_t len = hton64(iocb->payload_length);
|
||||
*buf_last |= 127;
|
||||
++buf_last;
|
||||
memcpy(buf_last, &len, 8);
|
||||
buf_last += 8;
|
||||
} else {
|
||||
/* Too large payload length */
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (iocb->mask) {
|
||||
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
|
||||
0) {
|
||||
return WSLAY_ERR_INVALID_CALLBACK;
|
||||
} else {
|
||||
ctx->omask = 1;
|
||||
memcpy(buf_last, ctx->omaskkey, 4);
|
||||
buf_last += 4;
|
||||
}
|
||||
}
|
||||
ctx->ostate = SEND_PAYLOAD;
|
||||
ctx->opayloadlen = iocb->payload_length;
|
||||
ctx->opayloadoff = 0;
|
||||
|
||||
buflen -= (size_t)(buf_last - buf);
|
||||
/* fall through */
|
||||
case SEND_PAYLOAD:
|
||||
if (iocb->data_length > 0) {
|
||||
size_t writelen = wslay_min(buflen, iocb->data_length);
|
||||
|
||||
if (ctx->omask) {
|
||||
for (i = 0; i < writelen; ++i) {
|
||||
*buf_last++ =
|
||||
iocb->data[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
|
||||
}
|
||||
} else {
|
||||
memcpy(buf_last, iocb->data, writelen);
|
||||
buf_last += writelen;
|
||||
}
|
||||
|
||||
ctx->opayloadoff += writelen;
|
||||
*pwpayloadlen = writelen;
|
||||
}
|
||||
|
||||
if (ctx->opayloadoff == ctx->opayloadlen) {
|
||||
ctx->ostate = PREP_HEADER;
|
||||
}
|
||||
|
||||
return buf_last - buf;
|
||||
default:
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
static void wslay_shift_ibuf(wslay_frame_context_ptr ctx) {
|
||||
ptrdiff_t len = ctx->ibuflimit - ctx->ibufmark;
|
||||
memmove(ctx->ibuf, ctx->ibufmark, (size_t)len);
|
||||
ctx->ibuflimit = ctx->ibuf + len;
|
||||
ctx->ibufmark = ctx->ibuf;
|
||||
}
|
||||
|
||||
static ssize_t wslay_recv(wslay_frame_context_ptr ctx) {
|
||||
ssize_t r;
|
||||
if (ctx->ibufmark != ctx->ibuf) {
|
||||
wslay_shift_ibuf(ctx);
|
||||
}
|
||||
r = ctx->callbacks.recv_callback(
|
||||
ctx->ibuflimit, (size_t)(ctx->ibuf + sizeof(ctx->ibuf) - ctx->ibuflimit),
|
||||
0, ctx->user_data);
|
||||
if (r > 0) {
|
||||
ctx->ibuflimit += r;
|
||||
} else {
|
||||
r = WSLAY_ERR_WANT_READ;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
#define WSLAY_AVAIL_IBUF(ctx) ((size_t)(ctx->ibuflimit - ctx->ibufmark))
|
||||
|
||||
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
|
||||
struct wslay_frame_iocb *iocb) {
|
||||
ssize_t r;
|
||||
if (ctx->istate == RECV_HEADER1) {
|
||||
uint8_t fin, opcode, rsv, payloadlen;
|
||||
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||
if ((r = wslay_recv(ctx)) <= 0) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||
return WSLAY_ERR_WANT_READ;
|
||||
}
|
||||
fin = (ctx->ibufmark[0] >> 7) & 1;
|
||||
rsv = (ctx->ibufmark[0] >> 4) & 7;
|
||||
opcode = ctx->ibufmark[0] & 0xfu;
|
||||
ctx->iom.opcode = opcode;
|
||||
ctx->iom.fin = fin;
|
||||
ctx->iom.rsv = rsv;
|
||||
++ctx->ibufmark;
|
||||
ctx->imask = (ctx->ibufmark[0] >> 7) & 1;
|
||||
payloadlen = ctx->ibufmark[0] & 0x7fu;
|
||||
++ctx->ibufmark;
|
||||
if (wslay_is_ctrl_frame(opcode) && (payloadlen > 125 || !fin)) {
|
||||
return WSLAY_ERR_PROTO;
|
||||
}
|
||||
if (payloadlen == 126) {
|
||||
ctx->istate = RECV_EXT_PAYLOADLEN;
|
||||
ctx->ireqread = 2;
|
||||
} else if (payloadlen == 127) {
|
||||
ctx->istate = RECV_EXT_PAYLOADLEN;
|
||||
ctx->ireqread = 8;
|
||||
} else {
|
||||
ctx->ipayloadlen = payloadlen;
|
||||
ctx->ipayloadoff = 0;
|
||||
if (ctx->imask) {
|
||||
ctx->istate = RECV_MASKKEY;
|
||||
ctx->ireqread = 4;
|
||||
} else {
|
||||
ctx->istate = RECV_PAYLOAD;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ctx->istate == RECV_EXT_PAYLOADLEN) {
|
||||
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||
if ((r = wslay_recv(ctx)) <= 0) {
|
||||
return r;
|
||||
}
|
||||
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||
return WSLAY_ERR_WANT_READ;
|
||||
}
|
||||
}
|
||||
ctx->ipayloadlen = 0;
|
||||
ctx->ipayloadoff = 0;
|
||||
memcpy((uint8_t *)&ctx->ipayloadlen + (8 - ctx->ireqread), ctx->ibufmark,
|
||||
ctx->ireqread);
|
||||
ctx->ipayloadlen = ntoh64(ctx->ipayloadlen);
|
||||
ctx->ibufmark += ctx->ireqread;
|
||||
if (ctx->ireqread == 8) {
|
||||
if (ctx->ipayloadlen < (1 << 16) || ctx->ipayloadlen & (1ull << 63)) {
|
||||
return WSLAY_ERR_PROTO;
|
||||
}
|
||||
} else if (ctx->ipayloadlen < 126) {
|
||||
return WSLAY_ERR_PROTO;
|
||||
}
|
||||
if (ctx->imask) {
|
||||
ctx->istate = RECV_MASKKEY;
|
||||
ctx->ireqread = 4;
|
||||
} else {
|
||||
ctx->istate = RECV_PAYLOAD;
|
||||
}
|
||||
}
|
||||
if (ctx->istate == RECV_MASKKEY) {
|
||||
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||
if ((r = wslay_recv(ctx)) <= 0) {
|
||||
return r;
|
||||
}
|
||||
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||
return WSLAY_ERR_WANT_READ;
|
||||
}
|
||||
}
|
||||
memcpy(ctx->imaskkey, ctx->ibufmark, 4);
|
||||
ctx->ibufmark += 4;
|
||||
ctx->istate = RECV_PAYLOAD;
|
||||
}
|
||||
if (ctx->istate == RECV_PAYLOAD) {
|
||||
uint8_t *readlimit, *readmark;
|
||||
uint64_t rempayloadlen = ctx->ipayloadlen - ctx->ipayloadoff;
|
||||
if (WSLAY_AVAIL_IBUF(ctx) == 0 && rempayloadlen > 0) {
|
||||
if ((r = wslay_recv(ctx)) <= 0) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
readmark = ctx->ibufmark;
|
||||
readlimit = WSLAY_AVAIL_IBUF(ctx) < rempayloadlen
|
||||
? ctx->ibuflimit
|
||||
: ctx->ibufmark + rempayloadlen;
|
||||
if (ctx->imask) {
|
||||
for (; ctx->ibufmark != readlimit; ++ctx->ibufmark, ++ctx->ipayloadoff) {
|
||||
ctx->ibufmark[0] ^= ctx->imaskkey[ctx->ipayloadoff % 4];
|
||||
}
|
||||
} else {
|
||||
ctx->ibufmark = readlimit;
|
||||
ctx->ipayloadoff += (uint64_t)(readlimit - readmark);
|
||||
}
|
||||
iocb->fin = ctx->iom.fin;
|
||||
iocb->rsv = ctx->iom.rsv;
|
||||
iocb->opcode = ctx->iom.opcode;
|
||||
iocb->payload_length = ctx->ipayloadlen;
|
||||
iocb->mask = ctx->imask;
|
||||
iocb->data = readmark;
|
||||
iocb->data_length = (size_t)(ctx->ibufmark - readmark);
|
||||
if (ctx->ipayloadlen == ctx->ipayloadoff) {
|
||||
ctx->istate = RECV_HEADER1;
|
||||
ctx->ireqread = 2;
|
||||
}
|
||||
return (ssize_t)iocb->data_length;
|
||||
}
|
||||
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef WSLAY_FRAME_H
|
||||
#define WSLAY_FRAME_H
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif /* HAVE_CONFIG_H */
|
||||
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
enum wslay_frame_state {
|
||||
PREP_HEADER,
|
||||
PREP_HEADER_NOBUF,
|
||||
SEND_HEADER,
|
||||
SEND_PAYLOAD,
|
||||
RECV_HEADER1,
|
||||
RECV_PAYLOADLEN,
|
||||
RECV_EXT_PAYLOADLEN,
|
||||
RECV_MASKKEY,
|
||||
RECV_PAYLOAD
|
||||
};
|
||||
|
||||
struct wslay_frame_opcode_memo {
|
||||
uint8_t fin;
|
||||
uint8_t opcode;
|
||||
uint8_t rsv;
|
||||
};
|
||||
|
||||
struct wslay_frame_context {
|
||||
uint8_t ibuf[4096];
|
||||
uint8_t *ibufmark;
|
||||
uint8_t *ibuflimit;
|
||||
struct wslay_frame_opcode_memo iom;
|
||||
uint64_t ipayloadlen;
|
||||
uint64_t ipayloadoff;
|
||||
uint8_t imask;
|
||||
uint8_t imaskkey[4];
|
||||
enum wslay_frame_state istate;
|
||||
size_t ireqread;
|
||||
|
||||
uint8_t oheader[14];
|
||||
uint8_t *oheadermark;
|
||||
uint8_t *oheaderlimit;
|
||||
uint64_t opayloadlen;
|
||||
uint64_t opayloadoff;
|
||||
uint8_t omask;
|
||||
uint8_t omaskkey[4];
|
||||
enum wslay_frame_state ostate;
|
||||
|
||||
struct wslay_frame_callbacks callbacks;
|
||||
void *user_data;
|
||||
};
|
||||
|
||||
#endif /* WSLAY_FRAME_H */
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2020 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef WSLAY_MACRO_H
|
||||
#define WSLAY_MACRO_H
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include "config.h"
|
||||
#endif /* HAVE_CONFIG_H */
|
||||
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#define wslay_struct_of(ptr, type, member) \
|
||||
((type *)(void *)((char *)(ptr)-offsetof(type, member)))
|
||||
|
||||
#endif /* WSLAY_MACRO_H */
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#include "wslay_net.h"
|
||||
|
||||
#ifndef WORDS_BIGENDIAN
|
||||
|
||||
uint64_t wslay_byteswap64(uint64_t x) {
|
||||
uint64_t u = ntohl(x & 0xffffffffllu);
|
||||
uint64_t l = ntohl((uint32_t)(x >> 32));
|
||||
return (u << 32) | l;
|
||||
}
|
||||
|
||||
#endif /* !WORDS_BIGENDIAN */
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef WSLAY_NET_H
|
||||
#define WSLAY_NET_H
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif /* HAVE_CONFIG_H */
|
||||
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
#ifdef HAVE_ARPA_INET_H
|
||||
# include <arpa/inet.h>
|
||||
#endif /* HAVE_ARPA_INET_H */
|
||||
#ifdef HAVE_NETINET_IN_H
|
||||
# include <netinet/in.h>
|
||||
#endif /* HAVE_NETINET_IN_H */
|
||||
/* For Mingw build */
|
||||
#ifdef HAVE_WINSOCK2_H
|
||||
# include <winsock2.h>
|
||||
#endif /* HAVE_WINSOCK2_H */
|
||||
|
||||
#ifdef WORDS_BIGENDIAN
|
||||
# define ntoh64(x) (x)
|
||||
# define hton64(x) (x)
|
||||
#else /* !WORDS_BIGENDIAN */
|
||||
uint64_t wslay_byteswap64(uint64_t x);
|
||||
# define ntoh64(x) wslay_byteswap64(x)
|
||||
# define hton64(x) wslay_byteswap64(x)
|
||||
#endif /* !WORDS_BIGENDIAN */
|
||||
|
||||
#endif /* WSLAY_NET_H */
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#include "wslay_queue.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "wslay_macro.h"
|
||||
|
||||
void wslay_queue_init(struct wslay_queue *queue) {
|
||||
queue->top = NULL;
|
||||
queue->tail = &queue->top;
|
||||
}
|
||||
|
||||
void wslay_queue_deinit(struct wslay_queue *queue) { (void)queue; }
|
||||
|
||||
void wslay_queue_push(struct wslay_queue *queue,
|
||||
struct wslay_queue_entry *ent) {
|
||||
ent->next = NULL;
|
||||
*queue->tail = ent;
|
||||
queue->tail = &ent->next;
|
||||
}
|
||||
|
||||
void wslay_queue_push_front(struct wslay_queue *queue,
|
||||
struct wslay_queue_entry *ent) {
|
||||
ent->next = queue->top;
|
||||
queue->top = ent;
|
||||
|
||||
if (ent->next == NULL) {
|
||||
queue->tail = &ent->next;
|
||||
}
|
||||
}
|
||||
|
||||
void wslay_queue_pop(struct wslay_queue *queue) {
|
||||
assert(queue->top);
|
||||
queue->top = queue->top->next;
|
||||
if (queue->top == NULL) {
|
||||
queue->tail = &queue->top;
|
||||
}
|
||||
}
|
||||
|
||||
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue) {
|
||||
assert(queue->top);
|
||||
return queue->top;
|
||||
}
|
||||
|
||||
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue) {
|
||||
assert(queue->top);
|
||||
return wslay_struct_of(queue->tail, struct wslay_queue_entry, next);
|
||||
}
|
||||
|
||||
int wslay_queue_empty(struct wslay_queue *queue) {
|
||||
assert(queue->top || queue->tail == &queue->top);
|
||||
return queue->top == NULL;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Wslay - The WebSocket Library
|
||||
*
|
||||
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
#ifndef WSLAY_QUEUE_H
|
||||
#define WSLAY_QUEUE_H
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include "config.h"
|
||||
#endif /* HAVE_CONFIG_H */
|
||||
|
||||
#include <wslay/wslay.h>
|
||||
|
||||
struct wslay_queue_entry {
|
||||
struct wslay_queue_entry *next;
|
||||
};
|
||||
|
||||
struct wslay_queue {
|
||||
struct wslay_queue_entry *top;
|
||||
struct wslay_queue_entry **tail;
|
||||
};
|
||||
|
||||
void wslay_queue_init(struct wslay_queue *queue);
|
||||
void wslay_queue_deinit(struct wslay_queue *queue);
|
||||
void wslay_queue_push(struct wslay_queue *queue, struct wslay_queue_entry *ent);
|
||||
void wslay_queue_push_front(struct wslay_queue *queue,
|
||||
struct wslay_queue_entry *ent);
|
||||
void wslay_queue_pop(struct wslay_queue *queue);
|
||||
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue);
|
||||
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue);
|
||||
int wslay_queue_empty(struct wslay_queue *queue);
|
||||
|
||||
#endif /* WSLAY_QUEUE_H */
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "util/utils.h"
|
||||
#include "util/detour.h"
|
||||
#include "acioemu/handle.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include "trackball.h"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "util/logging.h"
|
||||
#include "util/precise_timer.h"
|
||||
#include "util/memutils.h"
|
||||
#include "io.h"
|
||||
#include "motion_cam.h"
|
||||
#include "rgb_cam.h"
|
||||
|
||||
#pragma pack(push)
|
||||
@@ -276,48 +278,50 @@ namespace games::drs {
|
||||
void DRSGame::attach() {
|
||||
Game::attach();
|
||||
|
||||
// TouchSDK hooks
|
||||
detour::iat("??0TouchSDK@@QEAA@XZ",
|
||||
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z",
|
||||
(void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE);
|
||||
detour::iat("??1TouchSDK@@QEAA@XZ",
|
||||
(void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z",
|
||||
(void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ",
|
||||
(void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ",
|
||||
(void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
|
||||
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
|
||||
|
||||
if (!DISABLE_TOUCH) {
|
||||
// TouchSDK hooks
|
||||
detour::iat("??0TouchSDK@@QEAA@XZ",
|
||||
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z",
|
||||
(void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE);
|
||||
detour::iat("??1TouchSDK@@QEAA@XZ",
|
||||
(void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
|
||||
(void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z",
|
||||
(void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ",
|
||||
(void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ",
|
||||
(void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE);
|
||||
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
|
||||
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
|
||||
|
||||
start_touch();
|
||||
} else {
|
||||
log_info("drs", "no native input method detected");
|
||||
log_info("drs", "touch input for dance floor disabled");
|
||||
}
|
||||
|
||||
init_down_motion_hook();
|
||||
|
||||
if (RGB_CAMERA_HOOK) {
|
||||
init_rgb_camera_hook();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ std::vector<Button> &games::drs::get_buttons() {
|
||||
"P2 Up",
|
||||
"P2 Down",
|
||||
"P2 Left",
|
||||
"P2 Right"
|
||||
"P2 Right",
|
||||
"Down Motion"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +31,8 @@ std::vector<Button> &games::drs::get_buttons() {
|
||||
std::string games::drs::get_buttons_help() {
|
||||
// keep to max 100 characters wide
|
||||
return
|
||||
"Motion camera required for DOWN movement.\n"
|
||||
"Touchscreen supported for dance floor."
|
||||
"Touchscreen supported for dance floor.\n"
|
||||
"Down Motion applies to both players."
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace games::drs {
|
||||
P2_Up,
|
||||
P2_Down,
|
||||
P2_Left,
|
||||
P2_Right
|
||||
P2_Right,
|
||||
DownMotion
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "games/drs/motion_cam.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/sigscan.h"
|
||||
#include "io.h"
|
||||
|
||||
namespace games::drs {
|
||||
|
||||
// the game reads down movement from a depth camera whose driver is compiled into the game DLL,
|
||||
// so there is no import to replace.
|
||||
//
|
||||
// with no camera the tracked height never changes, so the motion state never leaves "still"
|
||||
// and DOWN notes can never be hit.
|
||||
//
|
||||
// rather than answer CInputManager::IsDown, the button drives the same state pair the camera
|
||||
// would; that leaves the game's own edge test untouched and keeps every note in a frame seeing
|
||||
// one answer.
|
||||
static int32_t *MOTION_STATE = nullptr;
|
||||
static int32_t *MOTION_STATE_PREV = nullptr;
|
||||
static constexpr int32_t MOTION_STATE_STILL = 1;
|
||||
static constexpr int32_t MOTION_STATE_DOWN = 2;
|
||||
|
||||
static void (*UpdateMotion_orig)() = nullptr;
|
||||
static bool DOWN_MOTION_HELD = false;
|
||||
|
||||
static void InputManager_UpdateMotion() {
|
||||
UpdateMotion_orig();
|
||||
|
||||
auto &buttons = get_buttons();
|
||||
const bool held = GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::DownMotion));
|
||||
|
||||
// a state of DOWN only counts as movement when the frame before it was something else
|
||||
if (held) {
|
||||
*MOTION_STATE_PREV = DOWN_MOTION_HELD ? MOTION_STATE_DOWN : MOTION_STATE_STILL;
|
||||
*MOTION_STATE = MOTION_STATE_DOWN;
|
||||
}
|
||||
|
||||
DOWN_MOTION_HELD = held;
|
||||
}
|
||||
|
||||
void init_down_motion_hook() {
|
||||
// only hook motion camera if the user has down motion button bound to something
|
||||
auto &buttons = get_buttons();
|
||||
if (!buttons.at(Buttons::DownMotion).isSet()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// CInputManager::IsDown - read the state pair out of its two operands.
|
||||
// Identical in game versions 2020121400 and 2022121400 / 2024120300:
|
||||
//
|
||||
// 83 3D xx xx xx xx 02 cmp dword [rip+prev], 2 ; state one frame ago
|
||||
// 74 0C je FALSE ; already down, so no new edge
|
||||
// 83 3D xx xx xx xx 02 cmp dword [rip+cur], 2 ; state this frame
|
||||
// 75 03 jne FALSE ; not down
|
||||
// B0 01 mov al, 1 ; went down on this frame
|
||||
// C3 ret
|
||||
// 32 C0 FALSE: xor al, al
|
||||
// C3 ret
|
||||
auto is_down = reinterpret_cast<uint8_t *>(find_pattern(
|
||||
avs::game::DLL_INSTANCE,
|
||||
"833D0000000002740C833D00000000027503B001C332C0C3",
|
||||
"XX????XXXXX????XXXXXXXXX",
|
||||
0, 0));
|
||||
|
||||
if (is_down == nullptr) {
|
||||
log_warning("drs", "motion sensor state not found, DOWN motion button unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
// rip-relative displacements resolve against the end of their own instruction, and each
|
||||
// cmp above is 7 bytes with its displacement 4 bytes in: the first ends at +7 with its
|
||||
// displacement at +2, the second starts at +9 so it ends at +16 with its own at +11.
|
||||
// read signed, since a target sitting below the instruction encodes as negative.
|
||||
MOTION_STATE_PREV = (int32_t *) (is_down + 7 + *(int32_t *) (is_down + 2));
|
||||
MOTION_STATE = (int32_t *) (is_down + 16 + *(int32_t *) (is_down + 11));
|
||||
|
||||
// CInputManager::UpdateMotion is hooked for the sole purpose of reading the Down Motion
|
||||
// button mapping frequently enough so that the rising edge can be detected.
|
||||
//
|
||||
// Matched over the prologue up to the camera read; the wildcards are a float and a callee:
|
||||
//
|
||||
// 48 83 EC 38 sub rsp, 0x38
|
||||
// 0F 29 74 24 20 movaps [rsp+0x20], xmm6
|
||||
// F3 0F 10 35 xx xx xx xx movss xmm6, [rip+height] ; reading as of last frame
|
||||
// E8 xx xx xx xx call GetPlayVideoProcess
|
||||
// 48 8B C8 mov rcx, rax
|
||||
// 48 8B 10 mov rdx, [rax]
|
||||
// FF 52 60 call [rdx+0x60] ; reading off the newest frame
|
||||
auto update = reinterpret_cast<void *>(find_pattern(
|
||||
avs::game::DLL_INSTANCE,
|
||||
"4883EC380F29742420F30F103500000000E800000000488BC8488B10FF5260",
|
||||
"XXXXXXXXXXXXX????X????XXXXXXXXX",
|
||||
0, 0));
|
||||
|
||||
if (update == nullptr ||
|
||||
!detour::trampoline_try(
|
||||
update, (void *) InputManager_UpdateMotion, (void **) &UpdateMotion_orig)) {
|
||||
log_warning("drs", "motion update not hooked, DOWN motion button unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("drs", "hooked DOWN motion at +{:#x}",
|
||||
(uintptr_t) ((uint8_t *) update - (uint8_t *) avs::game::DLL_INSTANCE));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
namespace games::drs {
|
||||
void init_down_motion_hook();
|
||||
}
|
||||
+171
-171
@@ -1,171 +1,171 @@
|
||||
#include "asio.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "gitadora.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
|
||||
// driver name in ASIO_DRIVER by intercepting registry calls to
|
||||
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
|
||||
// so we can recognise them on subsequent reg* calls.
|
||||
|
||||
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
|
||||
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
|
||||
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
|
||||
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
|
||||
|
||||
static HKEY real_asio_reg_handle = nullptr;
|
||||
static HKEY real_asio_device_reg_handle = nullptr;
|
||||
|
||||
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
|
||||
PHKEY phkResult)
|
||||
{
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == PARENT_ASIO_REG_HANDLE &&
|
||||
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
|
||||
|
||||
*phkResult = DEVICE_ASIO_REG_HANDLE;
|
||||
|
||||
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
|
||||
const auto result = RegOpenKeyExA_orig(
|
||||
real_asio_reg_handle,
|
||||
ASIO_DRIVER.value().c_str(),
|
||||
ulOptions,
|
||||
samDesired,
|
||||
&real_asio_device_reg_handle);
|
||||
|
||||
if (result != ERROR_SUCCESS) {
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"failed to open registry subkey '{}', error=0x{:x}",
|
||||
ASIO_DRIVER.value(), result);
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"due to improper ASIO setting, audio init will fail");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == HKEY_LOCAL_MACHINE &&
|
||||
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||
{
|
||||
*phkResult = PARENT_ASIO_REG_HANDLE;
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
|
||||
}
|
||||
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
|
||||
if (dwIndex == 0) {
|
||||
// forward to real handle just to verify the key exists; we
|
||||
// overwrite the name with our fake driver string regardless
|
||||
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
|
||||
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
|
||||
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
|
||||
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
|
||||
lpName[cchName - 1] = '\0';
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return ERROR_NO_MORE_ITEMS;
|
||||
}
|
||||
}
|
||||
|
||||
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
|
||||
LPBYTE lpData, LPDWORD lpcbData)
|
||||
{
|
||||
HKEY target = hKey;
|
||||
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpValueName != nullptr &&
|
||||
lpData != nullptr &&
|
||||
lpcbData != nullptr &&
|
||||
hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (_stricmp(lpValueName, "Description") == 0) {
|
||||
// engine may verify the driver name after open; ensure it still
|
||||
// sees something containing "XONAR" so the substring check passes
|
||||
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
|
||||
if (*lpcbData < len) {
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
return ERROR_MORE_DATA;
|
||||
}
|
||||
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
if (lpType != nullptr) {
|
||||
*lpType = REG_SZ;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// for everything else (CLSID etc.) defer to the real driver subkey
|
||||
target = real_asio_device_reg_handle;
|
||||
}
|
||||
|
||||
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE) {
|
||||
if (real_asio_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_reg_handle);
|
||||
real_asio_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
if (hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (real_asio_device_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_device_reg_handle);
|
||||
real_asio_device_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
return RegCloseKey_orig(hKey);
|
||||
}
|
||||
|
||||
void asio_hook_init() {
|
||||
if (!ASIO_DRIVER.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
|
||||
|
||||
RegCloseKey_orig = detour::iat_try(
|
||||
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
||||
RegEnumKeyA_orig = detour::iat_try(
|
||||
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyA_orig = detour::iat_try(
|
||||
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyExA_orig = detour::iat_try(
|
||||
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
||||
RegQueryValueExA_orig = detour::iat_try(
|
||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
#include "asio.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "gitadora.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
|
||||
// driver name in ASIO_DRIVER by intercepting registry calls to
|
||||
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
|
||||
// so we can recognise them on subsequent reg* calls.
|
||||
|
||||
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
|
||||
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
|
||||
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
|
||||
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
|
||||
|
||||
static HKEY real_asio_reg_handle = nullptr;
|
||||
static HKEY real_asio_device_reg_handle = nullptr;
|
||||
|
||||
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
|
||||
PHKEY phkResult)
|
||||
{
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == PARENT_ASIO_REG_HANDLE &&
|
||||
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
|
||||
|
||||
*phkResult = DEVICE_ASIO_REG_HANDLE;
|
||||
|
||||
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
|
||||
const auto result = RegOpenKeyExA_orig(
|
||||
real_asio_reg_handle,
|
||||
ASIO_DRIVER.value().c_str(),
|
||||
ulOptions,
|
||||
samDesired,
|
||||
&real_asio_device_reg_handle);
|
||||
|
||||
if (result != ERROR_SUCCESS) {
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"failed to open registry subkey '{}', error=0x{:x}",
|
||||
ASIO_DRIVER.value(), result);
|
||||
log_warning(
|
||||
"gitadora::asio",
|
||||
"due to improper ASIO setting, audio init will fail");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
hKey == HKEY_LOCAL_MACHINE &&
|
||||
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||
{
|
||||
*phkResult = PARENT_ASIO_REG_HANDLE;
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
|
||||
}
|
||||
|
||||
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
|
||||
if (dwIndex == 0) {
|
||||
// forward to real handle just to verify the key exists; we
|
||||
// overwrite the name with our fake driver string regardless
|
||||
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
|
||||
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
|
||||
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
|
||||
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
|
||||
lpName[cchName - 1] = '\0';
|
||||
}
|
||||
return ret;
|
||||
} else {
|
||||
return ERROR_NO_MORE_ITEMS;
|
||||
}
|
||||
}
|
||||
|
||||
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
|
||||
LPBYTE lpData, LPDWORD lpcbData)
|
||||
{
|
||||
HKEY target = hKey;
|
||||
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpValueName != nullptr &&
|
||||
lpData != nullptr &&
|
||||
lpcbData != nullptr &&
|
||||
hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (_stricmp(lpValueName, "Description") == 0) {
|
||||
// engine may verify the driver name after open; ensure it still
|
||||
// sees something containing "XONAR" so the substring check passes
|
||||
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
|
||||
if (*lpcbData < len) {
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
return ERROR_MORE_DATA;
|
||||
}
|
||||
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
|
||||
*lpcbData = static_cast<DWORD>(len);
|
||||
if (lpType != nullptr) {
|
||||
*lpType = REG_SZ;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// for everything else (CLSID etc.) defer to the real driver subkey
|
||||
target = real_asio_device_reg_handle;
|
||||
}
|
||||
|
||||
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
|
||||
}
|
||||
|
||||
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
|
||||
if (hKey == PARENT_ASIO_REG_HANDLE) {
|
||||
if (real_asio_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_reg_handle);
|
||||
real_asio_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
if (hKey == DEVICE_ASIO_REG_HANDLE) {
|
||||
if (real_asio_device_reg_handle != nullptr) {
|
||||
RegCloseKey_orig(real_asio_device_reg_handle);
|
||||
real_asio_device_reg_handle = nullptr;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
return RegCloseKey_orig(hKey);
|
||||
}
|
||||
|
||||
void asio_hook_init() {
|
||||
if (!ASIO_DRIVER.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
|
||||
|
||||
RegCloseKey_orig = detour::iat_try(
|
||||
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
||||
RegEnumKeyA_orig = detour::iat_try(
|
||||
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyA_orig = detour::iat_try(
|
||||
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
|
||||
RegOpenKeyExA_orig = detour::iat_try(
|
||||
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
||||
RegQueryValueExA_orig = detour::iat_try(
|
||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// installs IAT registry hooks in gfdm.dll that redirect the game's
|
||||
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
|
||||
// driver name read from games::gitadora::ASIO_DRIVER.
|
||||
//
|
||||
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
|
||||
// forward every call straight through to advapi32.
|
||||
void asio_hook_init();
|
||||
}
|
||||
#pragma once
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// installs IAT registry hooks in gfdm.dll that redirect the game's
|
||||
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
|
||||
// driver name read from games::gitadora::ASIO_DRIVER.
|
||||
//
|
||||
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
|
||||
// forward every call straight through to advapi32.
|
||||
void asio_hook_init();
|
||||
}
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
#include "asio.h"
|
||||
#include "handle.h"
|
||||
#include "bi2x_hook.h"
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "cfg/screen_resize.h"
|
||||
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/mme.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "overlay/overlay.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "util/cpuutils.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/detour.h"
|
||||
@@ -28,14 +31,45 @@ namespace games::gitadora {
|
||||
|
||||
// settings
|
||||
bool TWOCHANNEL = false;
|
||||
bool DISABLE_FRAME_LIMITER = false;
|
||||
std::optional<unsigned int> CAB_TYPE = std::nullopt;
|
||||
bool P1_LEFTY = false;
|
||||
bool P2_LEFTY = false;
|
||||
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
||||
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
||||
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
||||
ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
bool ALLOW_REALTEK_AUDIO = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
|
||||
std::pair<UINT, UINT> arena_subscreen_host_size() {
|
||||
if (GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
||||
return GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value();
|
||||
}
|
||||
if (arena_subscreen_landscape()) {
|
||||
return { ARENA_SUBSCREEN_LANDSCAPE_WIDTH, ARENA_SUBSCREEN_LANDSCAPE_HEIGHT };
|
||||
}
|
||||
return { ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT };
|
||||
}
|
||||
|
||||
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height) {
|
||||
if (host_width <= 0 || host_height <= 0) {
|
||||
return RECT {};
|
||||
}
|
||||
|
||||
LONG width = MulDiv(host_height, ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT);
|
||||
LONG height = host_height;
|
||||
if (width > host_width) {
|
||||
width = host_width;
|
||||
height = MulDiv(host_width, ARENA_SUBSCREEN_HEIGHT, ARENA_SUBSCREEN_WIDTH);
|
||||
}
|
||||
|
||||
const LONG left = (host_width - width) / 2;
|
||||
const LONG top = (host_height - height) / 2;
|
||||
return RECT { left, top, left + width, top + height };
|
||||
}
|
||||
|
||||
/*
|
||||
* Prevent GitaDora from creating folders on F drive
|
||||
@@ -61,6 +95,46 @@ namespace games::gitadora {
|
||||
|
||||
return CreateDirectoryA(lpPathName, lpSecurityAttributes);
|
||||
}
|
||||
|
||||
// libshare-pj paces mainloop with separate 12 ms and 16 ms waits. these waits
|
||||
// interfere with the game's normal display synchronization on modern Windows
|
||||
// and can hold a nominal 60 FPS game near 58 FPS. locate the instruction
|
||||
// sequences at runtime so the fix does not depend on per-version file offsets.
|
||||
static void disable_mainloop_frame_limiter(HMODULE sharepj_module) {
|
||||
if (!sharepj_module) {
|
||||
return;
|
||||
}
|
||||
|
||||
// both limiters have the same shape, only the millisecond target xx differs
|
||||
// (0Ch for the 12 ms limiter, 10h for the 16 ms one):
|
||||
//
|
||||
// 48 83 F8 xx: cmp rax, xx; compare elapsed frame time with the target
|
||||
// 73 10: jae +10h; skip the wait once the target has elapsed
|
||||
// B9 xx 00 00 00: mov ecx, xx; load the target
|
||||
// 48 2B C8: sub rcx, rax; calculate the remaining wait time
|
||||
// 74 06: je +6h; skip the following six-byte Sleep call if no wait remains
|
||||
//
|
||||
// changing jae (73h) to jmp (EBh) makes each block always take its existing
|
||||
// skip path, which bypasses only the associated Sleep call and leaves the 10h
|
||||
// branch displacement and every other wait untouched.
|
||||
const auto limiter_12ms_disabled = replace_pattern(
|
||||
sharepj_module,
|
||||
"4883F80C7310B90C000000482BC87406",
|
||||
"????????EB??????????????????????", 0, 0);
|
||||
const auto limiter_16ms_disabled = replace_pattern(
|
||||
sharepj_module,
|
||||
"4883F8107310B910000000482BC87406",
|
||||
"????????EB??????????????????????", 0, 0);
|
||||
|
||||
if (!limiter_12ms_disabled || !limiter_16ms_disabled) {
|
||||
log_fatal(
|
||||
"gitadora",
|
||||
"failed to disable libshare-pj mainloop frame limiter (-gdnoframelimiter), ensure patch is not already applied");
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("gitadora", "successfully disabled libshare-pj mainloop frame limiter (-gdnoframelimiter)");
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
@@ -270,9 +344,21 @@ namespace games::gitadora {
|
||||
break;
|
||||
case 2:
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
log_fatal(
|
||||
if (D3D9_ADAPTER.has_value()) {
|
||||
log_fatal(
|
||||
"gitadora",
|
||||
"arena model: fullscreen two-window mode cannot use -monitor; "
|
||||
"use -w for borderless windows instead");
|
||||
}
|
||||
ARENA_TWO_HEAD_EXCLUSIVE = true;
|
||||
log_info(
|
||||
"gitadora",
|
||||
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
|
||||
"arena model: native two-head fullscreen adapter-group mode "
|
||||
"(MAIN + SMALL; LEFT/RIGHT are virtual)");
|
||||
} else {
|
||||
log_info(
|
||||
"gitadora",
|
||||
"arena model: two-window mode uses windowed rendering");
|
||||
}
|
||||
log_info("gitadora", "arena model: two-window mode");
|
||||
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
|
||||
@@ -285,6 +371,23 @@ namespace games::gitadora {
|
||||
"gitadora",
|
||||
"arena model: unsupported window count: {}", count);
|
||||
}
|
||||
|
||||
if (arena_subscreen_landscape() && !ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||
log_warning(
|
||||
"gitadora",
|
||||
"arena model: landscape subscreen needs full screen two-window mode, ignoring");
|
||||
ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off;
|
||||
}
|
||||
if (ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||
const auto [host_width, host_height] = arena_subscreen_host_size();
|
||||
if (host_width != ARENA_SUBSCREEN_WIDTH
|
||||
|| host_height != ARENA_SUBSCREEN_HEIGHT) {
|
||||
log_info(
|
||||
"gitadora",
|
||||
"arena model: SMALL head runs at {}x{}, subscreen is scaled to fit",
|
||||
host_width, host_height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -298,10 +401,18 @@ namespace games::gitadora {
|
||||
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
|
||||
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
|
||||
|
||||
// cached primary real monitor: its path + source/target mode entries.
|
||||
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
|
||||
// Cached real monitor paths and their source/target mode entries. modeInfoIdx values are
|
||||
// renumbered so the cache is self-contained. The normal single-head emulation keeps only
|
||||
// MAIN; the two-head adapter-group mode keeps MAIN and SMALL as real heads.
|
||||
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
|
||||
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
|
||||
static DISPLAYCONFIG_PATH_INFO real_small_path = {};
|
||||
static DISPLAYCONFIG_MODE_INFO real_small_modes[2] = {}; // [0]=source, [1]=target
|
||||
static bool real_small_path_available = false;
|
||||
|
||||
static bool is_two_head_exclusive() {
|
||||
return ARENA_TWO_HEAD_EXCLUSIVE && !GRAPHICS_WINDOWED;
|
||||
}
|
||||
|
||||
// fake monitors appended after the real ones. the game classifies monitors
|
||||
// by outputTechnology + connectorInstance:
|
||||
@@ -325,12 +436,31 @@ namespace games::gitadora {
|
||||
// adapter to a swap chain role via its DisplayConfig connector instance,
|
||||
// so the entries here must be listed in the same order the wrapper enumerates
|
||||
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
|
||||
static constexpr FakeMonitor FAKE_MONITORS[] = {
|
||||
// Normal single-head emulation: every non-MAIN role is fake.
|
||||
static constexpr FakeMonitor FAKE_MONITORS_SINGLE_HEAD[] = {
|
||||
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
|
||||
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
|
||||
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
|
||||
};
|
||||
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
|
||||
|
||||
// Two-head fullscreen: adapters 0/1 are the real MAIN/SMALL group heads.
|
||||
// Keep only LEFT/RIGHT fake, with their adapter ids matching the D3D9 wrapper.
|
||||
static constexpr FakeMonitor FAKE_MONITORS_TWO_HEAD[] = {
|
||||
{ 2, 1080, 1920, -200000, -200000, 0 }, // left (DP connector instance 0)
|
||||
{ 3, 1080, 1920, -300000, -300000, 1 }, // right (DP connector instance 1)
|
||||
};
|
||||
|
||||
static std::span<const FakeMonitor> get_fake_monitors() {
|
||||
if (is_two_head_exclusive()) {
|
||||
return FAKE_MONITORS_TWO_HEAD;
|
||||
}
|
||||
|
||||
return FAKE_MONITORS_SINGLE_HEAD;
|
||||
}
|
||||
|
||||
static UINT32 real_monitor_count() {
|
||||
return is_two_head_exclusive() ? 2 : 1;
|
||||
}
|
||||
|
||||
// call QueryDisplayConfig once, keep only the primary monitor's path and its
|
||||
// two referenced modes (source + target). modeInfoIdx values are rewritten to
|
||||
@@ -368,13 +498,55 @@ namespace games::gitadora {
|
||||
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
|
||||
}
|
||||
|
||||
if (primary->targetInfo.modeInfoIdx >= all_modes.size()) {
|
||||
log_fatal("gitadora", "cache_primary_monitor_info: primary target mode is missing");
|
||||
}
|
||||
|
||||
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
|
||||
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
|
||||
real_primary_path = *primary;
|
||||
real_primary_path.sourceInfo.modeInfoIdx = 0;
|
||||
real_primary_path.targetInfo.modeInfoIdx = 1;
|
||||
|
||||
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
||||
real_small_path_available = false;
|
||||
if (is_two_head_exclusive()) {
|
||||
if (all_paths.size() != 2) {
|
||||
log_fatal(
|
||||
"gitadora",
|
||||
"two-head fullscreen mode requires exactly two active display paths, found {}",
|
||||
all_paths.size());
|
||||
}
|
||||
|
||||
const auto secondary = std::find_if(all_paths.begin(), all_paths.end(),
|
||||
[&](const auto &p) {
|
||||
return &p != &*primary;
|
||||
});
|
||||
if (secondary == all_paths.end() ||
|
||||
secondary->sourceInfo.modeInfoIdx >= all_modes.size() ||
|
||||
secondary->targetInfo.modeInfoIdx >= all_modes.size()) {
|
||||
log_fatal("gitadora", "could not identify the physical SMALL monitor");
|
||||
}
|
||||
const LUID primary_adapter = primary->sourceInfo.adapterId;
|
||||
const LUID secondary_adapter = secondary->sourceInfo.adapterId;
|
||||
const bool same_adapter = primary_adapter.HighPart == secondary_adapter.HighPart &&
|
||||
primary_adapter.LowPart == secondary_adapter.LowPart;
|
||||
if (!same_adapter) {
|
||||
log_fatal(
|
||||
"gitadora",
|
||||
"MAIN and SMALL must use the same display adapter");
|
||||
}
|
||||
|
||||
real_small_modes[0] = all_modes[secondary->sourceInfo.modeInfoIdx];
|
||||
real_small_modes[1] = all_modes[secondary->targetInfo.modeInfoIdx];
|
||||
real_small_path = *secondary;
|
||||
real_small_path.sourceInfo.modeInfoIdx = 2;
|
||||
real_small_path.targetInfo.modeInfoIdx = 3;
|
||||
real_small_path_available = true;
|
||||
|
||||
log_info("gitadora", "cache_primary_monitor_info: cached real MAIN and SMALL monitors");
|
||||
} else {
|
||||
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
@@ -389,33 +561,36 @@ namespace games::gitadora {
|
||||
static std::once_flag populate_once;
|
||||
std::call_once(populate_once, cache_primary_monitor_info);
|
||||
|
||||
// always report exactly 1 real + N fake monitors
|
||||
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
||||
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
||||
const auto fake_monitors = get_fake_monitors();
|
||||
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
|
||||
const UINT32 real_count = real_monitor_count();
|
||||
*pNumPathArrayElements = real_count + fake_count;
|
||||
*pNumModeInfoArrayElements = (real_count + fake_count) * 2;
|
||||
|
||||
log_info(
|
||||
"gitadora",
|
||||
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
|
||||
FAKE_MONITOR_COUNT);
|
||||
"GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)",
|
||||
real_count,
|
||||
fake_count);
|
||||
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
|
||||
// assumption): index 0 in both arrays holds the cached primary real monitor, so
|
||||
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
|
||||
// Write a fake monitor after the real paths. The cache is packed as [source, target]
|
||||
// mode pairs, so each path consumes two mode entries.
|
||||
static void insert_fake_monitor(
|
||||
DISPLAYCONFIG_PATH_INFO *paths,
|
||||
DISPLAYCONFIG_MODE_INFO *modes,
|
||||
UINT32 i)
|
||||
const FakeMonitor &m,
|
||||
UINT32 path_index,
|
||||
UINT32 source_mode_index)
|
||||
{
|
||||
const FakeMonitor &m = FAKE_MONITORS[i];
|
||||
const UINT32 src_idx = 2 + i * 2;
|
||||
const UINT32 src_idx = source_mode_index;
|
||||
const UINT32 tgt_idx = src_idx + 1;
|
||||
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
|
||||
const UINT32 uid = static_cast<UINT32>(-m.id);
|
||||
|
||||
paths[1 + i] = {
|
||||
paths[path_index] = {
|
||||
.sourceInfo = {
|
||||
.adapterId = adapter_id,
|
||||
.id = uid,
|
||||
@@ -473,21 +648,35 @@ namespace games::gitadora {
|
||||
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
|
||||
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
|
||||
{
|
||||
// copy cached primary real monitor into caller buffers at index 0
|
||||
// Copy cached real monitor paths into the caller buffers.
|
||||
pathArray[0] = real_primary_path;
|
||||
modeInfoArray[0] = real_primary_modes[0];
|
||||
modeInfoArray[1] = real_primary_modes[1];
|
||||
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
|
||||
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
|
||||
if (is_two_head_exclusive() && real_small_path_available) {
|
||||
pathArray[1] = real_small_path;
|
||||
modeInfoArray[2] = real_small_modes[0];
|
||||
modeInfoArray[3] = real_small_modes[1];
|
||||
}
|
||||
|
||||
const auto fake_monitors = get_fake_monitors();
|
||||
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
|
||||
const UINT32 real_count = real_monitor_count();
|
||||
*numPathArrayElements = real_count + fake_count;
|
||||
*numModeInfoArrayElements = (real_count + fake_count) * 2;
|
||||
if (currentTopologyId != nullptr) {
|
||||
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
|
||||
}
|
||||
|
||||
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
|
||||
|
||||
// append fake monitors after the real one
|
||||
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
|
||||
insert_fake_monitor(pathArray, modeInfoArray, i);
|
||||
// Append fake monitors after the real path(s).
|
||||
for (UINT32 i = 0; i < fake_count; i++) {
|
||||
insert_fake_monitor(
|
||||
pathArray,
|
||||
modeInfoArray,
|
||||
fake_monitors[i],
|
||||
real_count + i,
|
||||
real_count * 2 + i * 2);
|
||||
}
|
||||
|
||||
return ERROR_SUCCESS;
|
||||
@@ -518,7 +707,8 @@ namespace games::gitadora {
|
||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||
const LONG fake_id = -id;
|
||||
UINT32 conn_inst = 0xff;
|
||||
for (const auto& f : FAKE_MONITORS) {
|
||||
const auto fake_monitors = get_fake_monitors();
|
||||
for (const auto &f : fake_monitors) {
|
||||
if (f.id == fake_id) {
|
||||
conn_inst = f.connector_instance;
|
||||
break;
|
||||
@@ -541,18 +731,27 @@ namespace games::gitadora {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// override the cached primary real monitor target info to look like HDMI/0
|
||||
// Override MAIN to HDMI/0 and retag the real second head as the DP/2 SMALL
|
||||
// display expected by the cabinet software in two-head fullscreen mode.
|
||||
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
|
||||
const auto& target = real_primary_path.targetInfo;
|
||||
if (target.id == targetName->header.id &&
|
||||
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
||||
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
|
||||
{
|
||||
const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) {
|
||||
return target.id == targetName->header.id &&
|
||||
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
|
||||
target.adapterId.LowPart == targetName->header.adapterId.LowPart;
|
||||
};
|
||||
if (target_matches(real_primary_path.targetInfo)) {
|
||||
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
|
||||
targetName->connectorInstance = 0;
|
||||
log_info("gitadora",
|
||||
"overriding primary monitor (id={}) to pretend to be HDMI",
|
||||
targetName->header.id);
|
||||
} else if (is_two_head_exclusive() && real_small_path_available &&
|
||||
target_matches(real_small_path.targetInfo)) {
|
||||
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
|
||||
targetName->connectorInstance = 2;
|
||||
log_info("gitadora",
|
||||
"overriding secondary monitor (id={}) to pretend to be SMALL DP/2",
|
||||
targetName->header.id);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -575,6 +774,12 @@ namespace games::gitadora {
|
||||
HMODULE system_module = libutils::try_module("libsystem.dll");
|
||||
|
||||
// patches
|
||||
#ifdef SPICE64
|
||||
if (DISABLE_FRAME_LIMITER && !is_arena_model()) {
|
||||
disable_mainloop_frame_limiter(sharepj_module);
|
||||
}
|
||||
#endif
|
||||
|
||||
detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc(
|
||||
sharepj_module, "eam_network_detected_ip_change"));
|
||||
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
|
||||
@@ -650,31 +855,38 @@ namespace games::gitadora {
|
||||
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
|
||||
}
|
||||
|
||||
// monitor/touch hooks (windowed or full screen)
|
||||
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// enable touch hook for subscreen overlay
|
||||
// touch injection drives mouse-as-touch and API touch for the subscreen,
|
||||
// no matter whether it is drawn by the overlay (single-window mode) or by
|
||||
// the dedicated SMALL window
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
|
||||
// the legacy fallback can only feed the subscreen overlay
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
|
||||
}
|
||||
|
||||
#if !SPICE_XP
|
||||
|
||||
if (!GRAPHICS_WINDOWED) {
|
||||
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
|
||||
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
|
||||
GetDisplayConfigBufferSizes_orig =
|
||||
detour::iat_try("GetDisplayConfigBufferSizes",
|
||||
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
||||
QueryDisplayConfig_orig =
|
||||
detour::iat_try("QueryDisplayConfig",
|
||||
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
|
||||
DisplayConfigGetDeviceInfo_orig =
|
||||
detour::iat_try("DisplayConfigGetDeviceInfo",
|
||||
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
||||
if (!GRAPHICS_WINDOWED &&
|
||||
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) {
|
||||
if (ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||
log_info(
|
||||
"gitadora",
|
||||
"exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
|
||||
}
|
||||
#endif
|
||||
|
||||
GetDisplayConfigBufferSizes_orig =
|
||||
detour::iat_try("GetDisplayConfigBufferSizes",
|
||||
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
|
||||
QueryDisplayConfig_orig =
|
||||
detour::iat_try("QueryDisplayConfig",
|
||||
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
|
||||
DisplayConfigGetDeviceInfo_orig =
|
||||
detour::iat_try("DisplayConfigGetDeviceInfo",
|
||||
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// window patch
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
@@ -11,16 +12,52 @@
|
||||
|
||||
namespace games::gitadora {
|
||||
|
||||
// what a landscape monitor driving the SMALL head does with the space beside the
|
||||
// portrait subscreen image
|
||||
enum class ArenaSubscreenLandscape {
|
||||
Off, // SMALL head stays portrait
|
||||
Small, // subscreen centered, the space either side stays black
|
||||
All, // that space shows the LEFT and RIGHT heads instead
|
||||
};
|
||||
|
||||
// settings
|
||||
extern bool TWOCHANNEL;
|
||||
extern bool DISABLE_FRAME_LIMITER;
|
||||
extern std::optional<unsigned int> CAB_TYPE;
|
||||
extern bool P1_LEFTY;
|
||||
extern bool P2_LEFTY;
|
||||
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
|
||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
||||
extern ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern bool ALLOW_REALTEK_AUDIO;
|
||||
extern bool NATIVE_TOUCH;
|
||||
|
||||
static inline bool arena_subscreen_landscape() {
|
||||
return ARENA_SUBSCREEN_LANDSCAPE != ArenaSubscreenLandscape::Off;
|
||||
}
|
||||
|
||||
static inline bool arena_subscreen_shows_sides() {
|
||||
return ARENA_SUBSCREEN_LANDSCAPE == ArenaSubscreenLandscape::All;
|
||||
}
|
||||
|
||||
// arena SMALL subscreen (touch panel) resolution
|
||||
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
||||
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
|
||||
|
||||
// used when a landscape monitor drives the SMALL head and -forceressub is unset
|
||||
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_WIDTH = 1920;
|
||||
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_HEIGHT = 1080;
|
||||
|
||||
// resolution the SMALL head actually runs at; the portrait panel size unless
|
||||
// -forceressub or the landscape option overrides it
|
||||
std::pair<UINT, UINT> arena_subscreen_host_size();
|
||||
|
||||
// area of the host the portrait subscreen occupies, centered and aspect-preserved;
|
||||
// whatever is left over on either side stays black
|
||||
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height);
|
||||
|
||||
class GitaDoraGame : public games::Game {
|
||||
public:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#if SPICE64
|
||||
|
||||
#include <cstdint>
|
||||
#include "api/client.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
@@ -443,10 +444,12 @@ namespace games::iidx {
|
||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
|
||||
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
|
||||
|
||||
for (unsigned int i = 0; i < data_size; ++i) {
|
||||
map.data[i].r = data[i * 3];
|
||||
map.data[i].g = data[i * 3 + 1];
|
||||
map.data[i].b = data[i * 3 + 2];
|
||||
if (api::has_clients()) {
|
||||
for (size_t i = 0; i < data_size; ++i) {
|
||||
map.data[i].r = data[i * 3];
|
||||
map.data[i].g = data[i * 3 + 1];
|
||||
map.data[i].b = data[i * 3 + 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ namespace games::iidx {
|
||||
if (mediaTypePointer && mediaTypePointer->IsString()) {
|
||||
std::string mediaType = mediaTypePointer->GetString();
|
||||
if (mediaType.length() > 0) {
|
||||
camera->m_selectedMediaTypeDescription = mediaType;
|
||||
camera->SetSelectedMediaTypeDescription(mediaType);
|
||||
camera->m_useAutoMediaType = false;
|
||||
} else {
|
||||
camera->m_useAutoMediaType = true;
|
||||
@@ -547,7 +547,7 @@ namespace games::iidx {
|
||||
std::string drawModeString = drawModePointer->GetString();
|
||||
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
|
||||
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
|
||||
camera->m_drawMode = (LocalCameraDrawMode) j;
|
||||
camera->m_drawMode.store((LocalCameraDrawMode) j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -556,12 +556,12 @@ namespace games::iidx {
|
||||
// Flip
|
||||
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
|
||||
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
|
||||
camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
|
||||
camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
|
||||
}
|
||||
|
||||
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
|
||||
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
|
||||
camera->m_flipVertical = flipVerticalPointer->GetBool();
|
||||
camera->m_flipVertical.store(flipVerticalPointer->GetBool());
|
||||
}
|
||||
|
||||
// Allow manual control
|
||||
@@ -625,15 +625,17 @@ namespace games::iidx {
|
||||
if (camera->m_useAutoMediaType) {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, "");
|
||||
} else {
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
|
||||
const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
|
||||
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
|
||||
}
|
||||
|
||||
// Draw Mode
|
||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]);
|
||||
const auto drawMode = camera->m_drawMode.load();
|
||||
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[drawMode]);
|
||||
|
||||
// Flip
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);
|
||||
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
|
||||
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
|
||||
|
||||
// Manual control
|
||||
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
#include "hooks/sleephook.h"
|
||||
#include "launcher/options.h"
|
||||
#include "touch/touch.h"
|
||||
#include "misc/nativetouchhook.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "touch/native/nativetouchhook.h"
|
||||
#include "misc/eamuse.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/deferlog.h"
|
||||
#include "util/fileutils.h"
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "bi2x_hook.h"
|
||||
#include "ezusb.h"
|
||||
#include "io.h"
|
||||
#include "poke.h"
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
@@ -57,13 +58,14 @@ namespace games::iidx {
|
||||
|
||||
// settings
|
||||
bool FLIP_CAMS = false;
|
||||
std::optional<bool> DISABLE_CAMS;
|
||||
cab_camera_access_mode CAB_CAMERA_ACCESS = cab_camera_access_mode::automatic;
|
||||
bool TDJ_CAMERA = false;
|
||||
bool TDJ_CAMERA_PREFER_16_9 = true;
|
||||
bool TDJ_MODE = false;
|
||||
bool FORCE_720P = false;
|
||||
bool DISABLE_ESPEC_IO = false;
|
||||
bool NATIVE_TOUCH = false;
|
||||
bool NATIVE_TOUCH = true;
|
||||
bool ENABLE_POKE = false;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
|
||||
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
|
||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||
@@ -277,6 +279,26 @@ namespace games::iidx {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// best-effort read of a TDJ ROM file to determine if the game is running in TDJ mode
|
||||
//
|
||||
// these paths are not emulated - they hit whatever is actually mounted at that drive letter.
|
||||
// an empty optical or removable drive raises the modal "insert a disk" error (the launcher
|
||||
// clears SEM_FAILCRITICALERRORS process-wide) and a downed network drive stalls on redirector
|
||||
// timeouts, so only probe what a TDJ cabinet would actually be laid out on.
|
||||
// drive_path must be absolute and start with a drive letter.
|
||||
static bool tdj_rom_matches(const char *drive_path, const char *expected) {
|
||||
const wchar_t root[] = { (wchar_t) drive_path[0], L':', L'\\', L'\0' };
|
||||
const auto drive_type = GetDriveTypeW(root);
|
||||
|
||||
if (drive_type != DRIVE_FIXED && drive_type != DRIVE_RAMDISK) {
|
||||
log_misc("iidx", "not probing '{}' for TDJ, not a local disk (drive type {})",
|
||||
drive_path, drive_type);
|
||||
return false;
|
||||
}
|
||||
|
||||
return fileutils::text_read(drive_path) == expected;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
IIDXGame::IIDXGame() : Game("Beatmania IIDX") {
|
||||
@@ -337,8 +359,10 @@ namespace games::iidx {
|
||||
HAS_LIBAIO = true;
|
||||
|
||||
// check TDJ mode
|
||||
TDJ_MODE |= fileutils::text_read("C:\\000rom.txt") == "TDJ-JA";
|
||||
TDJ_MODE |= fileutils::text_read("D:\\001rom.txt") == "TDJ";
|
||||
if (!TDJ_MODE) {
|
||||
TDJ_MODE = tdj_rom_matches("C:\\000rom.txt", "TDJ-JA")
|
||||
|| tdj_rom_matches("D:\\001rom.txt", "TDJ");
|
||||
}
|
||||
|
||||
// force TDJ mode
|
||||
if (TDJ_MODE) {
|
||||
@@ -350,12 +374,15 @@ namespace games::iidx {
|
||||
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
|
||||
devicehook_init(avs::core::DLL_INSTANCE);
|
||||
|
||||
if (NATIVE_TOUCH) {
|
||||
nativetouchhook::hook(avs::game::DLL_INSTANCE);
|
||||
} else {
|
||||
NATIVE_TOUCH = !wintouchemu::FORCE &&
|
||||
nativetouch::hook(avs::game::DLL_INSTANCE);
|
||||
if (!NATIVE_TOUCH) {
|
||||
wintouchemu::FORCE = true;
|
||||
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
|
||||
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
|
||||
wintouchemu::hook_title_ends(
|
||||
"beatmania IIDX",
|
||||
"main",
|
||||
avs::game::DLL_INSTANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,10 +446,7 @@ namespace games::iidx {
|
||||
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
|
||||
|
||||
// check if cam hook should be enabled
|
||||
if (!DISABLE_CAMS.has_value()) {
|
||||
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
|
||||
}
|
||||
if (!DISABLE_CAMS.value()) {
|
||||
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
|
||||
init_legacy_camera_hook(FLIP_CAMS);
|
||||
}
|
||||
|
||||
@@ -449,19 +473,10 @@ namespace games::iidx {
|
||||
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
|
||||
}
|
||||
|
||||
// check for cab camera access for the second time (first time was in launcher.cpp)
|
||||
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab
|
||||
// therefore, start with cams OFF by default, and allow user to forcibly override to ON
|
||||
if (!games::iidx::DISABLE_CAMS.has_value()) {
|
||||
games::iidx::DISABLE_CAMS = true;
|
||||
if (options->at(launcher::Options::IIDXCabCamAccess).is_active() &&
|
||||
options->at(launcher::Options::IIDXCabCamAccess).value_text() == "on") {
|
||||
games::iidx::DISABLE_CAMS = false;
|
||||
}
|
||||
if (games::iidx::DISABLE_CAMS.value()) {
|
||||
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
|
||||
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||
}
|
||||
// auto with iidx module means turn off camera (non-cab use)
|
||||
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
|
||||
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
|
||||
SetEnvironmentVariable("CONNECT_CAMERA", "0");
|
||||
}
|
||||
|
||||
// windowed subscreen, enabled by default, unless turned off by user
|
||||
@@ -533,6 +548,12 @@ namespace games::iidx {
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::post_attach() {
|
||||
if (ENABLE_POKE) {
|
||||
poke::enable();
|
||||
}
|
||||
}
|
||||
|
||||
void IIDXGame::detach() {
|
||||
Game::detach();
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@
|
||||
|
||||
namespace games::iidx {
|
||||
|
||||
enum class cab_camera_access_mode {
|
||||
automatic,
|
||||
off,
|
||||
on,
|
||||
legacy,
|
||||
};
|
||||
|
||||
enum class iidx_aio_emulation_state {
|
||||
unknown,
|
||||
bi2a_com2,
|
||||
@@ -20,7 +27,7 @@ namespace games::iidx {
|
||||
// settings
|
||||
|
||||
extern bool FLIP_CAMS;
|
||||
extern std::optional<bool> DISABLE_CAMS;
|
||||
extern cab_camera_access_mode CAB_CAMERA_ACCESS;
|
||||
extern bool TDJ_CAMERA;
|
||||
extern bool TDJ_CAMERA_PREFER_16_9;
|
||||
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
|
||||
@@ -29,6 +36,7 @@ namespace games::iidx {
|
||||
extern bool FORCE_720P;
|
||||
extern bool DISABLE_ESPEC_IO;
|
||||
extern bool NATIVE_TOUCH;
|
||||
extern bool ENABLE_POKE;
|
||||
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
|
||||
extern std::optional<std::string> ASIO_DRIVER;
|
||||
extern uint8_t DIGITAL_TT_SENS;
|
||||
@@ -53,6 +61,7 @@ namespace games::iidx {
|
||||
|
||||
virtual void attach() override;
|
||||
virtual void pre_attach() override;
|
||||
virtual void post_attach() override;
|
||||
virtual void detach() override;
|
||||
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user