mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf86fbd238 | |||
| 3fdb369128 | |||
| db7defff5a | |||
| 451cbec0b9 | |||
| 5c69e295ab | |||
| 5065a92d55 | |||
| 5483e8e2c0 | |||
| e8949a2612 | |||
| db71a0b24d | |||
| d3d5422768 | |||
| 5185f753e5 |
@@ -408,6 +408,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/iidx/mf_wrappers.cpp
|
games/iidx/mf_wrappers.cpp
|
||||||
games/sdvx/bi2x_hook.cpp
|
games/sdvx/bi2x_hook.cpp
|
||||||
games/sdvx/sdvx.cpp
|
games/sdvx/sdvx.cpp
|
||||||
|
games/sdvx/sdvx_live2d.cpp
|
||||||
games/sdvx/io.cpp
|
games/sdvx/io.cpp
|
||||||
games/sdvx/camera.cpp
|
games/sdvx/camera.cpp
|
||||||
games/jb/jb.cpp
|
games/jb/jb.cpp
|
||||||
@@ -529,6 +530,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/graphics/nvenc_hook.cpp
|
hooks/graphics/nvenc_hook.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
hooks/graphics/backends/d3d9/d3d9_backend.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
hooks/graphics/backends/d3d9/d3d9_device.cpp
|
||||||
|
hooks/graphics/backends/d3d9/d3d9_live2d.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
hooks/graphics/backends/d3d9/d3d9_fake_swapchain.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
hooks/graphics/backends/d3d9/d3d9_swapchain.cpp
|
||||||
hooks/graphics/backends/d3d9/d3d9_texture.cpp
|
hooks/graphics/backends/d3d9/d3d9_texture.cpp
|
||||||
@@ -605,10 +607,15 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
overlay/windows/keypad.cpp
|
overlay/windows/keypad.cpp
|
||||||
overlay/windows/log.cpp
|
overlay/windows/log.cpp
|
||||||
overlay/windows/midi.cpp
|
overlay/windows/midi.cpp
|
||||||
|
overlay/windows/obs.cpp
|
||||||
|
overlay/windows/obs_websocket.cpp
|
||||||
overlay/windows/patch_manager.cpp
|
overlay/windows/patch_manager.cpp
|
||||||
overlay/windows/popn_sub.cpp
|
overlay/windows/popn_sub.cpp
|
||||||
overlay/windows/wnd_manager.cpp
|
overlay/windows/wnd_manager.cpp
|
||||||
|
|
||||||
|
# external
|
||||||
|
external/easywsclient/easywsclient.cpp
|
||||||
|
|
||||||
# rawinput
|
# rawinput
|
||||||
rawinput/rawinput.cpp
|
rawinput/rawinput.cpp
|
||||||
rawinput/sextet.cpp
|
rawinput/sextet.cpp
|
||||||
@@ -671,6 +678,33 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
|
|
||||||
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
|
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOURCE_FILES})
|
||||||
|
|
||||||
|
# guard against statically importing DLLs that must always be loaded dynamically:
|
||||||
|
# * DLLs users override via the modules directory (e.g. DXVK's d3d9.dll) - a
|
||||||
|
# static import loads the system copy at startup and preempts the override
|
||||||
|
# (issue #779).
|
||||||
|
# * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks
|
||||||
|
# Unity games.
|
||||||
|
# the check runs objdump on each produced binary and fails the build if any
|
||||||
|
# forbidden DLL is imported.
|
||||||
|
set(SPICE_FORBIDDEN_STATIC_IMPORTS
|
||||||
|
d3d8.dll d3d9.dll d3d10core.dll d3d11.dll dxgi.dll opengl32.dll
|
||||||
|
mf.dll mfplat.dll mfreadwrite.dll)
|
||||||
|
|
||||||
|
function(spice_guard_dll_imports target)
|
||||||
|
if(MSVC)
|
||||||
|
# objdump-based parsing assumes a GNU/LLVM toolchain
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
add_custom_command(TARGET ${target} POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-DOBJDUMP=${CMAKE_OBJDUMP}
|
||||||
|
-DTARGET_FILE=$<TARGET_FILE:${target}>
|
||||||
|
"-DFORBIDDEN=${SPICE_FORBIDDEN_STATIC_IMPORTS}"
|
||||||
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_no_static_dll_imports.cmake
|
||||||
|
VERBATIM
|
||||||
|
COMMENT "Checking ${target} for forbidden static DLL imports")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
# spice.exe / spice_laa.exe shared objects
|
# spice.exe / spice_laa.exe shared objects
|
||||||
###########################################
|
###########################################
|
||||||
# spice.exe and spice_laa.exe are compiled identically; the only difference is
|
# spice.exe and spice_laa.exe are compiled identically; the only difference is
|
||||||
@@ -955,3 +989,16 @@ set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stu
|
|||||||
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/archive64"
|
||||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
|
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64"
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/spicetools/64")
|
||||||
|
|
||||||
|
# forbidden static DLL import guard
|
||||||
|
###################################
|
||||||
|
# apply the check to every executable and DLL produced by this project.
|
||||||
|
get_property(spice_all_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS)
|
||||||
|
foreach(spice_target IN LISTS spice_all_targets)
|
||||||
|
get_target_property(spice_target_type ${spice_target} TYPE)
|
||||||
|
if(spice_target_type STREQUAL "EXECUTABLE"
|
||||||
|
OR spice_target_type STREQUAL "SHARED_LIBRARY"
|
||||||
|
OR spice_target_type STREQUAL "MODULE_LIBRARY")
|
||||||
|
spice_guard_dll_imports(${spice_target})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "util/libutils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace cfg {
|
namespace cfg {
|
||||||
@@ -35,7 +36,26 @@ namespace cfg {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION);
|
// load d3d9.dll dynamically rather than linking against it statically.
|
||||||
|
// a static import would force the system d3d9.dll to load at process
|
||||||
|
// startup for the main spice executable too (this file is shared with
|
||||||
|
// spice.exe), which loads system32\d3d9.dll before the modules directory
|
||||||
|
// is added to the DLL search path - preventing a user-supplied DXVK
|
||||||
|
// d3d9.dll in modules from ever loading for the game.
|
||||||
|
typedef IDirect3D9 *(WINAPI *Direct3DCreate9_t)(UINT);
|
||||||
|
HMODULE d3d9_module = libutils::try_library("d3d9.dll");
|
||||||
|
if (d3d9_module == nullptr) {
|
||||||
|
log_warning("configurator", "could not load d3d9.dll, falling back to software renderer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto Direct3DCreate9_fn =
|
||||||
|
reinterpret_cast<Direct3DCreate9_t>(libutils::try_proc(d3d9_module, "Direct3DCreate9"));
|
||||||
|
if (Direct3DCreate9_fn == nullptr) {
|
||||||
|
log_warning("configurator", "could not find Direct3DCreate9, falling back to software renderer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDirect3D9 *d3d = Direct3DCreate9_fn(D3D_SDK_VERSION);
|
||||||
if (d3d == nullptr) {
|
if (d3d == nullptr) {
|
||||||
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
|
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ static const char *CLASS_NAME = "ConfiguratorWindow";
|
|||||||
static std::string WINDOW_TITLE;
|
static std::string WINDOW_TITLE;
|
||||||
static int WINDOW_SIZE_X = 800;
|
static int WINDOW_SIZE_X = 800;
|
||||||
static int WINDOW_SIZE_Y = 600;
|
static int WINDOW_SIZE_Y = 600;
|
||||||
|
static const int WINDOW_MIN_SIZE_X = 540;
|
||||||
|
static const int WINDOW_MIN_SIZE_Y = 300;
|
||||||
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
|
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
|
||||||
|
|
||||||
static const UINT_PTR RENDER_TIMER_ID = 1;
|
static const UINT_PTR RENDER_TIMER_ID = 1;
|
||||||
@@ -85,6 +87,23 @@ static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
|
|||||||
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computes the top-left position that centers a window of the given size on
|
||||||
|
// the primary monitor's work area (the desktop minus the taskbar); falls back
|
||||||
|
// to (0, 0) if the monitor info can't be queried.
|
||||||
|
static POINT center_on_primary_monitor(int width, int height) {
|
||||||
|
POINT pos = { 0, 0 };
|
||||||
|
HMONITOR mon = MonitorFromPoint(pos, MONITOR_DEFAULTTOPRIMARY);
|
||||||
|
MONITORINFO mi {};
|
||||||
|
mi.cbSize = sizeof(mi);
|
||||||
|
if (GetMonitorInfo(mon, &mi)) {
|
||||||
|
const int work_w = mi.rcWork.right - mi.rcWork.left;
|
||||||
|
const int work_h = mi.rcWork.bottom - mi.rcWork.top;
|
||||||
|
pos.x = mi.rcWork.left + (work_w - width) / 2;
|
||||||
|
pos.y = mi.rcWork.top + (work_h - height) / 2;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
|
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
|
||||||
// clamped to a sane range. Falls back to 60 if detection fails or the value
|
// clamped to a sane range. Falls back to 60 if detection fails or the value
|
||||||
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
|
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
|
||||||
@@ -243,8 +262,10 @@ cfg::ConfiguratorWindow::~ConfiguratorWindow() {
|
|||||||
|
|
||||||
void cfg::ConfiguratorWindow::run() {
|
void cfg::ConfiguratorWindow::run() {
|
||||||
|
|
||||||
|
const POINT pos = center_on_primary_monitor(WINDOW_SIZE_X, WINDOW_SIZE_Y);
|
||||||
|
SetWindowPos(this->hWnd, HWND_TOP, pos.x, pos.y, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
|
||||||
|
|
||||||
// show window
|
// show window
|
||||||
SetWindowPos(this->hWnd, HWND_TOP, 0, 0, WINDOW_SIZE_X, WINDOW_SIZE_Y, 0);
|
|
||||||
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
ShowWindow(this->hWnd, SW_SHOWNORMAL);
|
||||||
UpdateWindow(this->hWnd);
|
UpdateWindow(this->hWnd);
|
||||||
|
|
||||||
@@ -290,6 +311,22 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case WM_GETMINMAXINFO: {
|
||||||
|
|
||||||
|
// enforce a minimum window size so the UI can't be shrunk to a
|
||||||
|
// point where the tabs/controls become unusable. The minimum is
|
||||||
|
// expressed in client-area pixels and converted to a full window
|
||||||
|
// size that accounts for the current frame/title-bar style.
|
||||||
|
RECT rc = { 0, 0, WINDOW_MIN_SIZE_X, WINDOW_MIN_SIZE_Y };
|
||||||
|
DWORD style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_STYLE));
|
||||||
|
DWORD ex_style = static_cast<DWORD>(GetWindowLongPtrW(hWnd, GWL_EXSTYLE));
|
||||||
|
if (AdjustWindowRectEx(&rc, style, FALSE, ex_style)) {
|
||||||
|
auto *mmi = reinterpret_cast<MINMAXINFO *>(lParam);
|
||||||
|
mmi->ptMinTrackSize.x = rc.right - rc.left;
|
||||||
|
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case WM_CREATE: {
|
case WM_CREATE: {
|
||||||
|
|
||||||
// set user data of window to class pointer
|
// set user data of window to class pointer
|
||||||
|
|||||||
@@ -0,0 +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}")
|
||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
// 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 "easywsclient.hpp"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#pragma comment( lib, "ws2_32" )
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
typedef SOCKET socket_t;
|
||||||
|
#ifndef _SSIZE_T_DEFINED
|
||||||
|
typedef int ssize_t;
|
||||||
|
#define _SSIZE_T_DEFINED
|
||||||
|
#endif
|
||||||
|
#ifndef _SOCKET_T_DEFINED
|
||||||
|
typedef SOCKET socket_t;
|
||||||
|
#define _SOCKET_T_DEFINED
|
||||||
|
#endif
|
||||||
|
#if defined(_MSC_VER) && !defined(snprintf)
|
||||||
|
#define snprintf _snprintf_s
|
||||||
|
#endif
|
||||||
|
#include <stdint.h>
|
||||||
|
#define socketerrno WSAGetLastError()
|
||||||
|
#define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS
|
||||||
|
#define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <errno.h>
|
||||||
|
typedef int socket_t;
|
||||||
|
#ifndef INVALID_SOCKET
|
||||||
|
#define INVALID_SOCKET (-1)
|
||||||
|
#endif
|
||||||
|
#ifndef SOCKET_ERROR
|
||||||
|
#define SOCKET_ERROR (-1)
|
||||||
|
#endif
|
||||||
|
#define closesocket(s) ::close(s)
|
||||||
|
#include <errno.h>
|
||||||
|
#define socketerrno errno
|
||||||
|
#define SOCKET_EAGAIN_EINPROGRESS EAGAIN
|
||||||
|
#define SOCKET_EWOULDBLOCK EWOULDBLOCK
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
// When false, easywsclient's diagnostics are suppressed. The host application
|
||||||
|
// opts in by setting this flag (see obs_websocket.cpp).
|
||||||
|
bool EASYWSCLIENT_LOGGING_ENABLED = false;
|
||||||
|
|
||||||
|
// Route easywsclient's diagnostic output through the project logger instead of
|
||||||
|
// writing to stderr, without editing the upstream source lines below: these two
|
||||||
|
// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc.
|
||||||
|
// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set.
|
||||||
|
static inline void easywsclient_logf(const char *fmt, ...) {
|
||||||
|
if (!EASYWSCLIENT_LOGGING_ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char buf[1024];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
// trim trailing newline(s); the logger appends its own
|
||||||
|
size_t len = strlen(buf);
|
||||||
|
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) {
|
||||||
|
buf[--len] = '\0';
|
||||||
|
}
|
||||||
|
log_misc("easywsclient", "{}", buf);
|
||||||
|
}
|
||||||
|
#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__)
|
||||||
|
#define fputs(str, stream) easywsclient_logf("%s", str)
|
||||||
|
|
||||||
|
using namespace easywsclient;
|
||||||
|
|
||||||
|
namespace { // private module-only namespace
|
||||||
|
|
||||||
|
socket_t hostname_connect(const std::string& hostname, int port) {
|
||||||
|
struct addrinfo hints;
|
||||||
|
struct addrinfo *result;
|
||||||
|
struct addrinfo *p;
|
||||||
|
int ret;
|
||||||
|
socket_t sockfd = INVALID_SOCKET;
|
||||||
|
char sport[16];
|
||||||
|
memset(&hints, 0, sizeof(hints));
|
||||||
|
hints.ai_family = AF_UNSPEC;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
snprintf(sport, 16, "%d", port);
|
||||||
|
if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0)
|
||||||
|
{
|
||||||
|
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for(p = result; p != NULL; p = p->ai_next)
|
||||||
|
{
|
||||||
|
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
|
||||||
|
if (sockfd == INVALID_SOCKET) { continue; }
|
||||||
|
if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
closesocket(sockfd);
|
||||||
|
sockfd = INVALID_SOCKET;
|
||||||
|
}
|
||||||
|
freeaddrinfo(result);
|
||||||
|
return sockfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _DummyWebSocket : public easywsclient::WebSocket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void poll(int timeout) { }
|
||||||
|
void send(const std::string& message) { }
|
||||||
|
void sendBinary(const std::string& message) { }
|
||||||
|
void sendBinary(const std::vector<uint8_t>& message) { }
|
||||||
|
void sendPing() { }
|
||||||
|
void close() { }
|
||||||
|
readyStateValues getReadyState() const { return CLOSED; }
|
||||||
|
void _dispatch(Callback_Imp & callable) { }
|
||||||
|
void _dispatchBinary(BytesCallback_Imp& callable) { }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class _RealWebSocket : public easywsclient::WebSocket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol
|
||||||
|
//
|
||||||
|
// 0 1 2 3
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
// +-+-+-+-+-------+-+-------------+-------------------------------+
|
||||||
|
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
||||||
|
// |I|S|S|S| (4) |A| (7) | (16/64) |
|
||||||
|
// |N|V|V|V| |S| | (if payload len==126/127) |
|
||||||
|
// | |1|2|3| |K| | |
|
||||||
|
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|
||||||
|
// | Extended payload length continued, if payload len == 127 |
|
||||||
|
// + - - - - - - - - - - - - - - - +-------------------------------+
|
||||||
|
// | |Masking-key, if MASK set to 1 |
|
||||||
|
// +-------------------------------+-------------------------------+
|
||||||
|
// | Masking-key (continued) | Payload Data |
|
||||||
|
// +-------------------------------- - - - - - - - - - - - - - - - +
|
||||||
|
// : Payload Data continued ... :
|
||||||
|
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|
||||||
|
// | Payload Data continued ... |
|
||||||
|
// +---------------------------------------------------------------+
|
||||||
|
|
||||||
|
struct wsheader_type {
|
||||||
|
unsigned header_size;
|
||||||
|
bool fin;
|
||||||
|
bool mask;
|
||||||
|
enum opcode_type {
|
||||||
|
CONTINUATION = 0x0,
|
||||||
|
TEXT_FRAME = 0x1,
|
||||||
|
BINARY_FRAME = 0x2,
|
||||||
|
CLOSE = 8,
|
||||||
|
PING = 9,
|
||||||
|
PONG = 0xa,
|
||||||
|
} opcode;
|
||||||
|
int N0;
|
||||||
|
uint64_t N;
|
||||||
|
uint8_t masking_key[4];
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<uint8_t> rxbuf;
|
||||||
|
std::vector<uint8_t> txbuf;
|
||||||
|
std::vector<uint8_t> receivedData;
|
||||||
|
|
||||||
|
socket_t sockfd;
|
||||||
|
readyStateValues readyState;
|
||||||
|
bool useMask;
|
||||||
|
bool isRxBad;
|
||||||
|
|
||||||
|
_RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) {
|
||||||
|
}
|
||||||
|
|
||||||
|
readyStateValues getReadyState() const {
|
||||||
|
return readyState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void poll(int timeout) { // timeout in milliseconds
|
||||||
|
if (readyState == CLOSED) {
|
||||||
|
if (timeout > 0) {
|
||||||
|
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
|
||||||
|
select(0, NULL, NULL, NULL, &tv);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (timeout != 0) {
|
||||||
|
fd_set rfds;
|
||||||
|
fd_set wfds;
|
||||||
|
timeval tv = { timeout/1000, (timeout%1000) * 1000 };
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_ZERO(&wfds);
|
||||||
|
FD_SET(sockfd, &rfds);
|
||||||
|
if (txbuf.size()) { FD_SET(sockfd, &wfds); }
|
||||||
|
select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL);
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
// FD_ISSET(0, &rfds) will be true
|
||||||
|
int N = rxbuf.size();
|
||||||
|
ssize_t ret;
|
||||||
|
rxbuf.resize(N + 1500);
|
||||||
|
ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0);
|
||||||
|
if (false) { }
|
||||||
|
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
|
||||||
|
rxbuf.resize(N);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (ret <= 0) {
|
||||||
|
rxbuf.resize(N);
|
||||||
|
closesocket(sockfd);
|
||||||
|
readyState = CLOSED;
|
||||||
|
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rxbuf.resize(N + ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (txbuf.size()) {
|
||||||
|
int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0);
|
||||||
|
if (false) { } // ??
|
||||||
|
else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (ret <= 0) {
|
||||||
|
closesocket(sockfd);
|
||||||
|
readyState = CLOSED;
|
||||||
|
fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
txbuf.erase(txbuf.begin(), txbuf.begin() + ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!txbuf.size() && readyState == CLOSING) {
|
||||||
|
closesocket(sockfd);
|
||||||
|
readyState = CLOSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void _dispatch(Callback_Imp & callable) {
|
||||||
|
struct CallbackAdapter : public BytesCallback_Imp
|
||||||
|
// Adapt void(const std::string<uint8_t>&) to void(const std::string&)
|
||||||
|
{
|
||||||
|
Callback_Imp& callable;
|
||||||
|
CallbackAdapter(Callback_Imp& callable) : callable(callable) { }
|
||||||
|
void operator()(const std::vector<uint8_t>& message) {
|
||||||
|
std::string stringMessage(message.begin(), message.end());
|
||||||
|
callable(stringMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
CallbackAdapter bytesCallback(callable);
|
||||||
|
_dispatchBinary(bytesCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void _dispatchBinary(BytesCallback_Imp& callable) {
|
||||||
|
// TODO: consider acquiring a lock on rxbuf...
|
||||||
|
while (true) {
|
||||||
|
wsheader_type ws;
|
||||||
|
if (rxbuf.size() < 2) { return; /* Need at least 2 */ }
|
||||||
|
const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume
|
||||||
|
ws.fin = (data[0] & 0x80) == 0x80;
|
||||||
|
ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f);
|
||||||
|
ws.mask = (data[1] & 0x80) == 0x80;
|
||||||
|
ws.N0 = (data[1] & 0x7f);
|
||||||
|
ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0);
|
||||||
|
if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ }
|
||||||
|
int i = 0;
|
||||||
|
if (ws.N0 < 126) {
|
||||||
|
ws.N = ws.N0;
|
||||||
|
i = 2;
|
||||||
|
}
|
||||||
|
else if (ws.N0 == 126) {
|
||||||
|
ws.N = 0;
|
||||||
|
ws.N |= ((uint64_t) data[2]) << 8;
|
||||||
|
ws.N |= ((uint64_t) data[3]) << 0;
|
||||||
|
i = 4;
|
||||||
|
}
|
||||||
|
else if (ws.N0 == 127) {
|
||||||
|
ws.N = 0;
|
||||||
|
ws.N |= ((uint64_t) data[2]) << 56;
|
||||||
|
ws.N |= ((uint64_t) data[3]) << 48;
|
||||||
|
ws.N |= ((uint64_t) data[4]) << 40;
|
||||||
|
ws.N |= ((uint64_t) data[5]) << 32;
|
||||||
|
ws.N |= ((uint64_t) data[6]) << 24;
|
||||||
|
ws.N |= ((uint64_t) data[7]) << 16;
|
||||||
|
ws.N |= ((uint64_t) data[8]) << 8;
|
||||||
|
ws.N |= ((uint64_t) data[9]) << 0;
|
||||||
|
i = 10;
|
||||||
|
}
|
||||||
|
if (ws.mask) {
|
||||||
|
ws.masking_key[0] = ((uint8_t) data[i+0]) << 0;
|
||||||
|
ws.masking_key[1] = ((uint8_t) data[i+1]) << 0;
|
||||||
|
ws.masking_key[2] = ((uint8_t) data[i+2]) << 0;
|
||||||
|
ws.masking_key[3] = ((uint8_t) data[i+3]) << 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ws.masking_key[0] = 0;
|
||||||
|
ws.masking_key[1] = 0;
|
||||||
|
ws.masking_key[2] = 0;
|
||||||
|
ws.masking_key[3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The checks above should hopefully ensure this addition
|
||||||
|
// cannot overflow:
|
||||||
|
if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ }
|
||||||
|
|
||||||
|
// We got a whole message, now do something with it:
|
||||||
|
if (false) { }
|
||||||
|
else if (
|
||||||
|
ws.opcode == wsheader_type::TEXT_FRAME
|
||||||
|
|| ws.opcode == wsheader_type::BINARY_FRAME
|
||||||
|
|| ws.opcode == wsheader_type::CONTINUATION
|
||||||
|
) {
|
||||||
|
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
|
||||||
|
receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed
|
||||||
|
if (ws.fin) {
|
||||||
|
callable((const std::vector<uint8_t>) receivedData);
|
||||||
|
receivedData.erase(receivedData.begin(), receivedData.end());
|
||||||
|
std::vector<uint8_t> ().swap(receivedData);// free memory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ws.opcode == wsheader_type::PING) {
|
||||||
|
if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } }
|
||||||
|
std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);
|
||||||
|
sendData(wsheader_type::PONG, data.size(), data.begin(), data.end());
|
||||||
|
}
|
||||||
|
else if (ws.opcode == wsheader_type::PONG) { }
|
||||||
|
else if (ws.opcode == wsheader_type::CLOSE) { close(); }
|
||||||
|
else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); }
|
||||||
|
|
||||||
|
rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendPing() {
|
||||||
|
std::string empty;
|
||||||
|
sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void send(const std::string& message) {
|
||||||
|
sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendBinary(const std::string& message) {
|
||||||
|
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendBinary(const std::vector<uint8_t>& message) {
|
||||||
|
sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Iterator>
|
||||||
|
void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) {
|
||||||
|
// TODO:
|
||||||
|
// Masking key should (must) be derived from a high quality random
|
||||||
|
// number generator, to mitigate attacks on non-WebSocket friendly
|
||||||
|
// middleware:
|
||||||
|
const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 };
|
||||||
|
// TODO: consider acquiring a lock on txbuf...
|
||||||
|
if (readyState == CLOSING || readyState == CLOSED) { return; }
|
||||||
|
std::vector<uint8_t> header;
|
||||||
|
header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0);
|
||||||
|
header[0] = 0x80 | type;
|
||||||
|
if (false) { }
|
||||||
|
else if (message_size < 126) {
|
||||||
|
header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0);
|
||||||
|
if (useMask) {
|
||||||
|
header[2] = masking_key[0];
|
||||||
|
header[3] = masking_key[1];
|
||||||
|
header[4] = masking_key[2];
|
||||||
|
header[5] = masking_key[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message_size < 65536) {
|
||||||
|
header[1] = 126 | (useMask ? 0x80 : 0);
|
||||||
|
header[2] = (message_size >> 8) & 0xff;
|
||||||
|
header[3] = (message_size >> 0) & 0xff;
|
||||||
|
if (useMask) {
|
||||||
|
header[4] = masking_key[0];
|
||||||
|
header[5] = masking_key[1];
|
||||||
|
header[6] = masking_key[2];
|
||||||
|
header[7] = masking_key[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // TODO: run coverage testing here
|
||||||
|
header[1] = 127 | (useMask ? 0x80 : 0);
|
||||||
|
header[2] = (message_size >> 56) & 0xff;
|
||||||
|
header[3] = (message_size >> 48) & 0xff;
|
||||||
|
header[4] = (message_size >> 40) & 0xff;
|
||||||
|
header[5] = (message_size >> 32) & 0xff;
|
||||||
|
header[6] = (message_size >> 24) & 0xff;
|
||||||
|
header[7] = (message_size >> 16) & 0xff;
|
||||||
|
header[8] = (message_size >> 8) & 0xff;
|
||||||
|
header[9] = (message_size >> 0) & 0xff;
|
||||||
|
if (useMask) {
|
||||||
|
header[10] = masking_key[0];
|
||||||
|
header[11] = masking_key[1];
|
||||||
|
header[12] = masking_key[2];
|
||||||
|
header[13] = masking_key[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// N.B. - txbuf will keep growing until it can be transmitted over the socket:
|
||||||
|
txbuf.insert(txbuf.end(), header.begin(), header.end());
|
||||||
|
txbuf.insert(txbuf.end(), message_begin, message_end);
|
||||||
|
if (useMask) {
|
||||||
|
size_t message_offset = txbuf.size() - message_size;
|
||||||
|
for (size_t i = 0; i != message_size; ++i) {
|
||||||
|
txbuf[message_offset + i] ^= masking_key[i&0x3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() {
|
||||||
|
if(readyState == CLOSING || readyState == CLOSED) { return; }
|
||||||
|
readyState = CLOSING;
|
||||||
|
uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key
|
||||||
|
std::vector<uint8_t> header(closeFrame, closeFrame+6);
|
||||||
|
txbuf.insert(txbuf.end(), header.begin(), header.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) {
|
||||||
|
char host[512];
|
||||||
|
int port;
|
||||||
|
char path[512];
|
||||||
|
if (url.size() >= 512) {
|
||||||
|
fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (origin.size() >= 200) {
|
||||||
|
fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (false) { }
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) {
|
||||||
|
}
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) {
|
||||||
|
port = 80;
|
||||||
|
}
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) {
|
||||||
|
path[0] = '\0';
|
||||||
|
}
|
||||||
|
else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) {
|
||||||
|
port = 80;
|
||||||
|
path[0] = '\0';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
//fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path);
|
||||||
|
socket_t sockfd = hostname_connect(host, port);
|
||||||
|
if (sockfd == INVALID_SOCKET) {
|
||||||
|
fprintf(stderr, "Unable to connect to %s:%d\n", host, port);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// XXX: this should be done non-blocking,
|
||||||
|
char line[1024];
|
||||||
|
int status;
|
||||||
|
int i;
|
||||||
|
snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
if (port == 80) {
|
||||||
|
snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
}
|
||||||
|
snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
if (!origin.empty()) {
|
||||||
|
snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
}
|
||||||
|
snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0);
|
||||||
|
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
|
||||||
|
line[i] = 0;
|
||||||
|
if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; }
|
||||||
|
if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; }
|
||||||
|
// TODO: verify response headers,
|
||||||
|
while (true) {
|
||||||
|
for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } }
|
||||||
|
if (line[0] == '\r' && line[1] == '\n') { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int flag = 1;
|
||||||
|
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm
|
||||||
|
#ifdef _WIN32
|
||||||
|
u_long on = 1;
|
||||||
|
ioctlsocket(sockfd, FIONBIO, &on);
|
||||||
|
#else
|
||||||
|
fcntl(sockfd, F_SETFL, O_NONBLOCK);
|
||||||
|
#endif
|
||||||
|
//fprintf(stderr, "Connected to: %s\n", url.c_str());
|
||||||
|
return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end of module-only namespace
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
namespace easywsclient {
|
||||||
|
|
||||||
|
WebSocket::pointer WebSocket::create_dummy() {
|
||||||
|
static pointer dummy = pointer(new _DummyWebSocket);
|
||||||
|
return dummy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) {
|
||||||
|
return ::from_url(url, true, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) {
|
||||||
|
return ::from_url(url, false, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace easywsclient
|
||||||
@@ -0,0 +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 */
|
||||||
@@ -33,8 +33,9 @@ namespace games::ccj {
|
|||||||
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
|
static UINT WINAPI GetRawInputDeviceList_hook(PRAWINPUTDEVICELIST pRawInputDeviceList, PUINT puiNumDevices,
|
||||||
UINT cbSize) {
|
UINT cbSize) {
|
||||||
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
|
auto result = GetRawInputDeviceList_orig(pRawInputDeviceList, puiNumDevices, cbSize);
|
||||||
if (result == 0xFFFFFFFF)
|
if (result == 0xFFFFFFFF) {
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (pRawInputDeviceList == NULL) {
|
if (pRawInputDeviceList == NULL) {
|
||||||
(*puiNumDevices)++;
|
(*puiNumDevices)++;
|
||||||
@@ -47,8 +48,9 @@ namespace games::ccj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
|
static UINT WINAPI GetRawInputDeviceInfoW_hook(HANDLE hDevice, UINT uiCommand, LPVOID pData, PUINT pcbSize) {
|
||||||
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME)
|
if (hDevice != fakeHandle || uiCommand != RIDI_DEVICENAME) {
|
||||||
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
|
return GetRawInputDeviceInfoW_orig(hDevice, uiCommand, pData, pcbSize);
|
||||||
|
}
|
||||||
|
|
||||||
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
|
const auto requiredLen = (wcslen(fakeDeviceName) + 1) * sizeof(wchar_t);
|
||||||
|
|
||||||
@@ -68,23 +70,157 @@ namespace games::ccj {
|
|||||||
|
|
||||||
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
|
static LONG_PTR WINAPI SetWindowLongPtrW_hook(HWND _hWnd, int nIndex, LONG_PTR dwNewLong) {
|
||||||
wchar_t buffer[256];
|
wchar_t buffer[256];
|
||||||
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName))
|
if (nIndex != GWLP_WNDPROC || GetWindowTextW(_hWnd, buffer, 256) == 0 || !wcswcs(buffer, windowName)) {
|
||||||
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
||||||
|
}
|
||||||
|
|
||||||
hWnd = _hWnd;
|
hWnd = _hWnd;
|
||||||
wndProc = (WNDPROC)dwNewLong;
|
wndProc = (WNDPROC)dwNewLong;
|
||||||
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
return SetWindowLongPtrW_orig(_hWnd, nIndex, dwNewLong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compute the cursor wrap region in client coordinates. The cursor is confined to the
|
||||||
|
// monitor, so if the window's client area extends past a screen edge (window larger than /
|
||||||
|
// offset off the monitor) the cursor can never reach that far client edge. Clamp the region
|
||||||
|
// to the on-screen portion of the client area so the right/bottom edges wrap as reliably as
|
||||||
|
// the left/top edges.
|
||||||
|
static RECT trackball_wrap_bounds(HWND wnd, const RECT &client) {
|
||||||
|
RECT bounds = client;
|
||||||
|
|
||||||
|
POINT origin = { 0, 0 };
|
||||||
|
ClientToScreen(wnd, &origin);
|
||||||
|
RECT clientScreen = {
|
||||||
|
origin.x,
|
||||||
|
origin.y,
|
||||||
|
origin.x + client.right,
|
||||||
|
origin.y + client.bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
MONITORINFO mi = {};
|
||||||
|
mi.cbSize = sizeof(mi);
|
||||||
|
RECT usable;
|
||||||
|
if (GetMonitorInfo(MonitorFromWindow(wnd, MONITOR_DEFAULTTONEAREST), &mi)
|
||||||
|
&& IntersectRect(&usable, &clientScreen, &mi.rcMonitor)) {
|
||||||
|
bounds.left = usable.left - origin.x;
|
||||||
|
bounds.top = usable.top - origin.y;
|
||||||
|
bounds.right = usable.right - origin.x;
|
||||||
|
bounds.bottom = usable.bottom - origin.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drive the trackball from the physical mouse cursor, wrapping it at the window edges so it
|
||||||
|
// can spin indefinitely. gated by the secondary-mouse button (hold or debounced toggle).
|
||||||
|
static void trackball_mouse_input(RAWMOUSE &rawMouse) {
|
||||||
|
static bool active = false;
|
||||||
|
static bool lastState = false;
|
||||||
|
static std::chrono::steady_clock::time_point lastModified = std::chrono::steady_clock::now();
|
||||||
|
static const std::chrono::milliseconds debounceDuration(100);
|
||||||
|
|
||||||
|
const auto currentTime = std::chrono::steady_clock::now();
|
||||||
|
const bool pressed = get_async_secondary_mouse();
|
||||||
|
const bool focused = GetForegroundWindow() == hWnd;
|
||||||
|
|
||||||
|
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
||||||
|
active = !active;
|
||||||
|
lastModified = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool engaged = focused
|
||||||
|
&& ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed));
|
||||||
|
if (!engaged) {
|
||||||
|
if (lastState && !active) {
|
||||||
|
lastState = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT cursor;
|
||||||
|
RECT client;
|
||||||
|
GetClientRect(hWnd, &client);
|
||||||
|
GetCursorPos(&cursor);
|
||||||
|
ScreenToClient(hWnd, &cursor);
|
||||||
|
|
||||||
|
const RECT bounds = trackball_wrap_bounds(hWnd, client);
|
||||||
|
|
||||||
|
static int lastX = cursor.x;
|
||||||
|
static int lastY = cursor.y;
|
||||||
|
if (!lastState) {
|
||||||
|
lastX = cursor.x;
|
||||||
|
lastY = cursor.y;
|
||||||
|
lastState = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||||
|
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||||
|
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
||||||
|
|
||||||
|
// wrap the cursor to the opposite edge once it reaches a boundary, so the trackball
|
||||||
|
// can keep spinning past the screen edge.
|
||||||
|
bool updateCursor = false;
|
||||||
|
auto wrap = [&updateCursor](LONG value, LONG lo, LONG hi) -> LONG {
|
||||||
|
if (value <= lo) {
|
||||||
|
updateCursor = true;
|
||||||
|
return hi - 5;
|
||||||
|
}
|
||||||
|
if (value >= hi - 1) {
|
||||||
|
updateCursor = true;
|
||||||
|
return lo + 5;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
cursor.x = wrap(cursor.x, bounds.left, bounds.right);
|
||||||
|
cursor.y = wrap(cursor.y, bounds.top, bounds.bottom);
|
||||||
|
|
||||||
|
lastX = cursor.x;
|
||||||
|
lastY = cursor.y;
|
||||||
|
|
||||||
|
if (updateCursor) {
|
||||||
|
ClientToScreen(hWnd, &cursor);
|
||||||
|
SetCursorPos(cursor.x, cursor.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drive the trackball from the configured analog axes / direction buttons.
|
||||||
|
static void trackball_mapped_input(RAWMOUSE &rawMouse) {
|
||||||
|
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
||||||
|
|
||||||
|
auto &analogs = get_analogs();
|
||||||
|
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
|
||||||
|
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
|
||||||
|
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
|
||||||
|
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
|
||||||
|
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &buttons = get_buttons();
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up])) {
|
||||||
|
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down])) {
|
||||||
|
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left])) {
|
||||||
|
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right])) {
|
||||||
|
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
|
static UINT WINAPI GetRawInputData_hook(HRAWINPUT hRawInput, UINT uiCommand, LPVOID pData, PUINT pcbSize, UINT cbSizeHeader) {
|
||||||
if (hRawInput != fakeHandle)
|
if (hRawInput != fakeHandle) {
|
||||||
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
|
return GetRawInputData_orig(hRawInput, uiCommand, pData, pcbSize, cbSizeHeader);
|
||||||
|
}
|
||||||
|
|
||||||
if (pData == NULL) {
|
if (pData == NULL) {
|
||||||
if (uiCommand == RID_HEADER)
|
if (uiCommand == RID_HEADER) {
|
||||||
*pcbSize = sizeof(RAWINPUTHEADER);
|
*pcbSize = sizeof(RAWINPUTHEADER);
|
||||||
else
|
} else {
|
||||||
*pcbSize = sizeof(RAWINPUT);
|
*pcbSize = sizeof(RAWINPUT);
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,92 +243,9 @@ namespace games::ccj {
|
|||||||
RAWMOUSE rawMouse {};
|
RAWMOUSE rawMouse {};
|
||||||
|
|
||||||
if (MOUSE_TRACKBALL) {
|
if (MOUSE_TRACKBALL) {
|
||||||
static bool active = false;
|
trackball_mouse_input(rawMouse);
|
||||||
static bool lastState = false;
|
|
||||||
|
|
||||||
static std::chrono::time_point<std::chrono::steady_clock> lastModified = std::chrono::steady_clock::now();
|
|
||||||
static std::chrono::milliseconds debounceDuration(100);
|
|
||||||
auto currentTime = std::chrono::steady_clock::now();
|
|
||||||
bool pressed = get_async_secondary_mouse();
|
|
||||||
bool focused = GetForegroundWindow() == hWnd;
|
|
||||||
|
|
||||||
if (focused && MOUSE_TRACKBALL_USE_TOGGLE && pressed && (currentTime - lastModified > debounceDuration)) {
|
|
||||||
active = !active;
|
|
||||||
lastModified = currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focused && ((MOUSE_TRACKBALL_USE_TOGGLE && active) || (!MOUSE_TRACKBALL_USE_TOGGLE && pressed))) {
|
|
||||||
POINT cursor;
|
|
||||||
RECT client;
|
|
||||||
|
|
||||||
GetClientRect(hWnd, &client);
|
|
||||||
int sx = client.right - client.left;
|
|
||||||
int sy = client.bottom - client.top;
|
|
||||||
|
|
||||||
GetCursorPos(&cursor);
|
|
||||||
ScreenToClient(hWnd, &cursor);
|
|
||||||
|
|
||||||
static int lastX = cursor.x;
|
|
||||||
static int lastY = cursor.y;
|
|
||||||
|
|
||||||
if (!lastState) {
|
|
||||||
lastX = cursor.x;
|
|
||||||
lastY = cursor.y;
|
|
||||||
lastState = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
|
||||||
rawMouse.lLastX = (int)((float)(cursor.x - lastX) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
|
||||||
rawMouse.lLastY = (int)((float)(lastY - cursor.y) * (float)TRACKBALL_SENSITIVITY / 20.0f);
|
|
||||||
|
|
||||||
bool updateCursor = false;
|
|
||||||
|
|
||||||
if (cursor.x <= 0) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.x = sx - 5;
|
|
||||||
} else if (cursor.x >= sx - 1) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.x = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor.y <= 0) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.y = sy - 5;
|
|
||||||
} else if (cursor.y >= sy - 1) {
|
|
||||||
updateCursor = true;
|
|
||||||
cursor.y = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastX = cursor.x;
|
|
||||||
lastY = cursor.y;
|
|
||||||
|
|
||||||
if (updateCursor) {
|
|
||||||
ClientToScreen(hWnd, &cursor);
|
|
||||||
SetCursorPos(cursor.x, cursor.y);
|
|
||||||
}
|
|
||||||
} else if (lastState && !active) {
|
|
||||||
lastState = false;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
rawMouse.usFlags = MOUSE_MOVE_RELATIVE;
|
trackball_mapped_input(rawMouse);
|
||||||
|
|
||||||
auto &analogs = get_analogs();
|
|
||||||
if (analogs[Analogs::Trackball_DX].isSet() || analogs[Analogs::Trackball_DY].isSet()) {
|
|
||||||
float x = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DX]) * 2.0f - 1.0f;
|
|
||||||
float y = GameAPI::Analogs::getState(RI_MGR, analogs[Analogs::Trackball_DY]) * 2.0f - 1.0f;
|
|
||||||
rawMouse.lLastX = (long) (x * (float) TRACKBALL_SENSITIVITY);
|
|
||||||
rawMouse.lLastY = (long) (-y * (float) TRACKBALL_SENSITIVITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto &buttons = get_buttons();
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Up]))
|
|
||||||
rawMouse.lLastY = TRACKBALL_SENSITIVITY;
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Down]))
|
|
||||||
rawMouse.lLastY = -TRACKBALL_SENSITIVITY;
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Left]))
|
|
||||||
rawMouse.lLastX = -TRACKBALL_SENSITIVITY;
|
|
||||||
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Trackball_Right]))
|
|
||||||
rawMouse.lLastX = TRACKBALL_SENSITIVITY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*((RAWINPUT*)pData) = { header, { rawMouse } };
|
*((RAWINPUT*)pData) = { header, { rawMouse } };
|
||||||
@@ -224,9 +277,8 @@ namespace games::ccj {
|
|||||||
static bool initialized = false;
|
static bool initialized = false;
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
initialized = true;
|
|
||||||
}
|
}
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
// announce
|
// announce
|
||||||
log_info("trackball", "init");
|
log_info("trackball", "init");
|
||||||
@@ -246,8 +298,6 @@ namespace games::ccj {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void trackball_thread_start() {
|
void trackball_thread_start() {
|
||||||
using namespace std::chrono_literals;
|
|
||||||
|
|
||||||
tbThreadRunning = true;
|
tbThreadRunning = true;
|
||||||
|
|
||||||
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
|
log_info("trackball", "thread start, use mouse: {}, toggle: {}", MOUSE_TRACKBALL, MOUSE_TRACKBALL_USE_TOGGLE);
|
||||||
@@ -259,8 +309,9 @@ namespace games::ccj {
|
|||||||
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tbThreadRunning)
|
if (!tbThreadRunning) {
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
timer.sleep(10);
|
timer.sleep(10);
|
||||||
}
|
}
|
||||||
@@ -269,8 +320,9 @@ namespace games::ccj {
|
|||||||
|
|
||||||
void trackball_thread_stop() {
|
void trackball_thread_stop() {
|
||||||
tbThreadRunning = false;
|
tbThreadRunning = false;
|
||||||
if (tbThread)
|
if (tbThread) {
|
||||||
tbThread->join();
|
tbThread->join();
|
||||||
|
}
|
||||||
|
|
||||||
log_info("trackball", "thread stop");
|
log_info("trackball", "thread stop");
|
||||||
|
|
||||||
|
|||||||
@@ -474,6 +474,8 @@ namespace games {
|
|||||||
vkey_defaults.push_back(VK_F12);
|
vkey_defaults.push_back(VK_F12);
|
||||||
names.emplace_back("Toggle Camera Control");
|
names.emplace_back("Toggle Camera Control");
|
||||||
vkey_defaults.push_back(0xFF);
|
vkey_defaults.push_back(0xFF);
|
||||||
|
names.emplace_back("Toggle OBS Control");
|
||||||
|
vkey_defaults.push_back(0xFF);
|
||||||
names.emplace_back("Player 1 PIN Macro");
|
names.emplace_back("Player 1 PIN Macro");
|
||||||
vkey_defaults.push_back(0xFF);
|
vkey_defaults.push_back(0xFF);
|
||||||
names.emplace_back("Player 2 PIN Macro");
|
names.emplace_back("Player 2 PIN Macro");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace games {
|
|||||||
ToggleScreenResize,
|
ToggleScreenResize,
|
||||||
ToggleFps,
|
ToggleFps,
|
||||||
ToggleCameraControl,
|
ToggleCameraControl,
|
||||||
|
ToggleOBSControl,
|
||||||
TriggerPinMacroP1,
|
TriggerPinMacroP1,
|
||||||
TriggerPinMacroP2,
|
TriggerPinMacroP2,
|
||||||
ScreenResize,
|
ScreenResize,
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include "sdvx_live2d.h"
|
||||||
|
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
|
||||||
|
// compiled out of 32-bit builds.
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "launcher/logger.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace games::sdvx {
|
||||||
|
|
||||||
|
// Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option).
|
||||||
|
//
|
||||||
|
// the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in
|
||||||
|
// <SCENE>". several scenes correspond to in-song gameplay (with the heavy
|
||||||
|
// Live2D rendering); we watch those log lines and keep the shared flag
|
||||||
|
// the d3d9 backend reads up to date. the hook never alters the log output
|
||||||
|
// (always returns false).
|
||||||
|
static bool live2d_scene_log_hook(
|
||||||
|
void *user, const std::string &data, logger::Style style, std::string &out) {
|
||||||
|
// any of these scenes counts as in-song gameplay (different play modes)
|
||||||
|
static const char *const gameplay_scenes[] = {
|
||||||
|
"in ALTERNATIVE_GAME_SCENE",
|
||||||
|
"in MEGAMIX_GAME_SCENE",
|
||||||
|
"in MEGAMIX_BATTLE",
|
||||||
|
"in BATTLE_GAME_SCENE",
|
||||||
|
"in AUTOMATION_GAME_SCENE",
|
||||||
|
"in ARENA_GAME_SCENE",
|
||||||
|
};
|
||||||
|
bool in_gameplay_scene = false;
|
||||||
|
for (const auto *scene : gameplay_scenes) {
|
||||||
|
if (data.find(scene) != std::string::npos) {
|
||||||
|
in_gameplay_scene = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!in_gameplay_scene) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// note: log messages here must NOT contain any matched scene token, else
|
||||||
|
// this hook would re-enter itself when the message is pushed.
|
||||||
|
if (data.find("I:Attach: in ") != std::string::npos) {
|
||||||
|
if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) {
|
||||||
|
log_info("sdvx", "Live2D skip: entering gameplay");
|
||||||
|
}
|
||||||
|
} else if (data.find("I:Detach: in ") != std::string::npos) {
|
||||||
|
if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) {
|
||||||
|
log_info("sdvx", "Live2D skip: leaving gameplay");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void live2d_scene_detection_init() {
|
||||||
|
static bool installed = false;
|
||||||
|
if (installed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installed = true;
|
||||||
|
// the logger's hook list is a persistent static, so registering here is
|
||||||
|
// safe even though this runs before logger::start(). we intentionally do
|
||||||
|
// NOT log a confirmation now: at this point the log file isn't open yet
|
||||||
|
// and the message would be dropped. the entering/leaving-gameplay lines
|
||||||
|
// above provide runtime confirmation once the logger is running.
|
||||||
|
logger::hook_add(live2d_scene_log_hook, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // SPICE64
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::sdvx {
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
// installs the Live2D in-game scene-detection log hook used by the
|
||||||
|
// -sdvxnolive2d "ingame" option. does not require the SDVX game module to
|
||||||
|
// be attached, so it can be enabled purely from the launcher option.
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so this is compiled out
|
||||||
|
// of 32-bit builds.
|
||||||
|
void live2d_scene_detection_init();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1273,9 +1273,11 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
|
|||||||
wintouchemu::update();
|
wintouchemu::update();
|
||||||
|
|
||||||
// newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW
|
// newer versions of exceed gear needs SUBSCREEN_FORCE_REDRAW
|
||||||
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen
|
// (when enabled on older versions of EG, you end up with graphical glitches on the subscreen)
|
||||||
// early versions of popn HC needs this as well, otherwise the subscreen doesn't update at all
|
//
|
||||||
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW || games::popn::is_pikapika_model()) {
|
// early versions of popn HC needs this as well, but not on by default as it can cause
|
||||||
|
// graphical glitches on some GPUs
|
||||||
|
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
|
||||||
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
|
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
#include "d3d9_device.h"
|
#include "d3d9_device.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <climits>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
@@ -12,6 +16,7 @@
|
|||||||
#include "cfg/screen_resize.h"
|
#include "cfg/screen_resize.h"
|
||||||
|
|
||||||
#include "d3d9_backend.h"
|
#include "d3d9_backend.h"
|
||||||
|
#include "d3d9_live2d.h"
|
||||||
#include "d3d9_texture.h"
|
#include "d3d9_texture.h"
|
||||||
|
|
||||||
#ifndef SPICE64
|
#ifndef SPICE64
|
||||||
@@ -1301,6 +1306,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitive(
|
|||||||
UINT PrimitiveCount)
|
UINT PrimitiveCount)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount));
|
CHECK_RESULT(pReal->DrawPrimitive(PrimitiveType, StartVertex, PrimitiveCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,6 +1321,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitive(
|
|||||||
UINT PrimitiveCount)
|
UINT PrimitiveCount)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawIndexedPrimitive(
|
CHECK_RESULT(pReal->DrawIndexedPrimitive(
|
||||||
PrimitiveType, BaseVertexIndex,
|
PrimitiveType, BaseVertexIndex,
|
||||||
MinVertexIndex, NumVertices,
|
MinVertexIndex, NumVertices,
|
||||||
@@ -1328,6 +1339,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawPrimitiveUP(
|
|||||||
WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})",
|
WRAP_DEBUG_FMT("DrawPrimitiveUP({}, {}, {}, {})",
|
||||||
PrimitiveType, PrimitiveCount,
|
PrimitiveType, PrimitiveCount,
|
||||||
fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride);
|
fmt::ptr(pVertexStreamZeroData), VertexStreamZeroStride);
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawPrimitiveUP(
|
CHECK_RESULT(pReal->DrawPrimitiveUP(
|
||||||
PrimitiveType, PrimitiveCount,
|
PrimitiveType, PrimitiveCount,
|
||||||
pVertexStreamZeroData, VertexStreamZeroStride));
|
pVertexStreamZeroData, VertexStreamZeroStride));
|
||||||
@@ -1344,6 +1358,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::DrawIndexedPrimitiveUP(
|
|||||||
UINT VertexStreamZeroStride)
|
UINT VertexStreamZeroStride)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG;
|
WRAP_DEBUG;
|
||||||
|
if (d3d9_live2d::should_skip_draw()) [[unlikely]] {
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->DrawIndexedPrimitiveUP(
|
CHECK_RESULT(pReal->DrawIndexedPrimitiveUP(
|
||||||
PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData,
|
PrimitiveType, MinVertexIndex, NumVertices, PrimitiveCount, pIndexData,
|
||||||
IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride));
|
IndexDataFormat, pVertexStreamZeroData, VertexStreamZeroStride));
|
||||||
@@ -1403,7 +1420,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateVertexShader(
|
|||||||
IDirect3DVertexShader9 **ppShader)
|
IDirect3DVertexShader9 **ppShader)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction));
|
WRAP_VERBOSE_FMT("CreateVertexShader({})", fmt::ptr(pFunction));
|
||||||
CHECK_RESULT(pReal->CreateVertexShader(pFunction, ppShader));
|
HRESULT ret = pReal->CreateVertexShader(pFunction, ppShader);
|
||||||
|
if (SUCCEEDED(ret) && ppShader != nullptr) {
|
||||||
|
d3d9_live2d::on_create_vertex_shader(*ppShader, pFunction);
|
||||||
|
}
|
||||||
|
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
|
||||||
|
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
||||||
@@ -1411,6 +1435,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetVertexShader(
|
|||||||
{
|
{
|
||||||
WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader));
|
WRAP_DEBUG_FMT("SetVertexShader({})", fmt::ptr(pShader));
|
||||||
|
|
||||||
|
d3d9_live2d::on_set_vertex_shader(pShader);
|
||||||
|
|
||||||
#ifndef SPICE64
|
#ifndef SPICE64
|
||||||
|
|
||||||
// diagonal line fix
|
// diagonal line fix
|
||||||
@@ -1557,13 +1583,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreatePixelShader(
|
|||||||
IDirect3DPixelShader9 **ppShader)
|
IDirect3DPixelShader9 **ppShader)
|
||||||
{
|
{
|
||||||
WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction));
|
WRAP_VERBOSE_FMT("CreatePixelShader({})", fmt::ptr(pFunction));
|
||||||
CHECK_RESULT(pReal->CreatePixelShader(pFunction, ppShader));
|
HRESULT ret = pReal->CreatePixelShader(pFunction, ppShader);
|
||||||
|
if (SUCCEEDED(ret) && ppShader != nullptr) {
|
||||||
|
d3d9_live2d::on_create_pixel_shader(*ppShader, pFunction);
|
||||||
|
}
|
||||||
|
if (GRAPHICS_LOG_HRESULT && FAILED(ret)) [[unlikely]] {
|
||||||
|
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(ret));
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::SetPixelShader(
|
||||||
IDirect3DPixelShader9 *pShader)
|
IDirect3DPixelShader9 *pShader)
|
||||||
{
|
{
|
||||||
WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader));
|
WRAP_DEBUG_FMT("SetPixelShader({})", fmt::ptr(pShader));
|
||||||
|
d3d9_live2d::on_set_pixel_shader(pShader);
|
||||||
CHECK_RESULT(pReal->SetPixelShader(pShader));
|
CHECK_RESULT(pReal->SetPixelShader(pShader));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#include "d3d9_live2d.h"
|
||||||
|
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation
|
||||||
|
// is compiled out of 32-bit builds (the header supplies inline no-op stubs there).
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
|
|
||||||
|
// how the Live2D draw filtering works
|
||||||
|
// ------------------------------------
|
||||||
|
// SDVX draws its Live2D characters with a small, fixed set of pixel and
|
||||||
|
// vertex shaders. to skip those draws (and save GPU) we have to recognise them at
|
||||||
|
// the exact moment the game issues a draw call. the d3d9 device hooks feed three
|
||||||
|
// kinds of events into this module:
|
||||||
|
//
|
||||||
|
// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader)
|
||||||
|
// the game compiles its shaders once at load. we can't trust the shader
|
||||||
|
// *object pointer* to identify a shader (it's just a heap address that
|
||||||
|
// varies per run and can be recycled), so instead we hash the shader's
|
||||||
|
// D3D9 *bytecode* - that fingerprint is stable across runs because the
|
||||||
|
// game ships the same shaders. if the hash matches a known Live2D shader
|
||||||
|
// we remember that object pointer in g_live2d_shaders.
|
||||||
|
//
|
||||||
|
// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader)
|
||||||
|
// whenever the game binds a shader we look it up in that set once and cache
|
||||||
|
// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen
|
||||||
|
// far less often than draws, so this is where the lookup cost lives.
|
||||||
|
//
|
||||||
|
// 3. draw call (should_skip_draw, called from every Draw* hook)
|
||||||
|
// the per-draw question "is this a Live2D draw?" is then just reading those
|
||||||
|
// two cached bools - no hashing, no map lookups. if the skip is currently
|
||||||
|
// active (see graphics_sdvx_live2d_should_skip) and either bound shader is
|
||||||
|
// Live2D, the Draw* hook drops the call instead of forwarding it.
|
||||||
|
//
|
||||||
|
// everything is gated on the feature being enabled (mode != Off); when it's Off
|
||||||
|
// every entry point is a single predicted-not-taken branch. d3d9 rendering for a
|
||||||
|
// device is single-threaded, so none of this state needs locking.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// shader state is tracked whenever the feature might act (mode != Off) so the
|
||||||
|
// known-shader set is populated before a song starts. when Off, every entry
|
||||||
|
// point is a single cheap branch.
|
||||||
|
bool tracking_enabled() {
|
||||||
|
return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the set of shader objects (pixel or vertex) whose bytecode matched a known
|
||||||
|
// Live2D fingerprint. only matching shaders are stored, so this stays tiny.
|
||||||
|
std::unordered_set<void *> g_live2d_shaders;
|
||||||
|
|
||||||
|
// whether the currently-bound shaders are known Live2D shaders. cached at set
|
||||||
|
// time so the per-draw check is just two bool reads.
|
||||||
|
bool g_cur_ps_is_live2d = false;
|
||||||
|
bool g_cur_vs_is_live2d = false;
|
||||||
|
|
||||||
|
// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF)
|
||||||
|
uint64_t bytecode_hash(const DWORD *func) {
|
||||||
|
if (func == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const DWORD *p = func;
|
||||||
|
const DWORD *cap = func + 65536; // safety bound
|
||||||
|
while (p < cap && *p != 0x0000FFFF) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD);
|
||||||
|
uint64_t h = 1469598103934665603ULL;
|
||||||
|
const auto *bytes = reinterpret_cast<const uint8_t *>(func);
|
||||||
|
for (size_t i = 0; i < n_bytes; i++) {
|
||||||
|
h ^= bytes[i];
|
||||||
|
h *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable
|
||||||
|
// across runs because the game ships fixed shaders. the two sets are disjoint so
|
||||||
|
// a single shader can be classified by its own hash alone.
|
||||||
|
bool hash_is_live2d(uint64_t hash) {
|
||||||
|
switch (hash) {
|
||||||
|
case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song)
|
||||||
|
case 0x2d7ce428c6b4775dULL: // pixel: masked model draw
|
||||||
|
case 0x3ce00cc6111c10e7ULL: // pixel: mask generation
|
||||||
|
case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant)
|
||||||
|
case 0xe9cf898c331e2a51ULL: // vertex
|
||||||
|
case 0x94dc84e7b7c0f437ULL: // vertex
|
||||||
|
case 0xc872937c5cc04309ULL: // vertex
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// classify a shader at creation time and record it if it is Live2D. erasing on a
|
||||||
|
// miss keeps the set correct if the runtime reuses a freed shader pointer.
|
||||||
|
void classify_shader(void *shader, const DWORD *func) {
|
||||||
|
if (hash_is_live2d(bytecode_hash(func))) {
|
||||||
|
g_live2d_shaders.insert(shader);
|
||||||
|
} else {
|
||||||
|
g_live2d_shaders.erase(shader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace d3d9_live2d {
|
||||||
|
|
||||||
|
// stage 1: fingerprint each shader as the game creates it
|
||||||
|
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) {
|
||||||
|
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
|
||||||
|
classify_shader(shader, func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) {
|
||||||
|
if (tracking_enabled() && shader != nullptr) [[unlikely]] {
|
||||||
|
classify_shader(shader, func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage 2: remember whether the just-bound shader is a Live2D one
|
||||||
|
void on_set_vertex_shader(IDirect3DVertexShader9 *shader) {
|
||||||
|
if (tracking_enabled()) [[unlikely]] {
|
||||||
|
g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_set_pixel_shader(IDirect3DPixelShader9 *shader) {
|
||||||
|
if (tracking_enabled()) [[unlikely]] {
|
||||||
|
g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage 3: drop the draw if the skip is active and a Live2D shader is bound
|
||||||
|
bool should_skip_draw() {
|
||||||
|
return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace d3d9_live2d
|
||||||
|
|
||||||
|
#endif // SPICE64
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <d3d9.h>
|
||||||
|
|
||||||
|
// SDVX Live2D draw-skip support for the D3D9 backend.
|
||||||
|
//
|
||||||
|
// SDVX renders its Live2D navigator / in-song character through a fixed set of
|
||||||
|
// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip)
|
||||||
|
// the matching draw calls are dropped to save GPU. shaders are identified by a
|
||||||
|
// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode
|
||||||
|
// does not). the hashes were captured with the draw-call fingerprinting tool.
|
||||||
|
//
|
||||||
|
// every entry point is a no-op unless the feature is enabled (mode != Off), and
|
||||||
|
// d3d9 rendering for a device is single-threaded, so none of this needs locking.
|
||||||
|
namespace d3d9_live2d {
|
||||||
|
|
||||||
|
#ifdef SPICE64
|
||||||
|
|
||||||
|
// record a shader's bytecode fingerprint at creation time
|
||||||
|
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func);
|
||||||
|
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func);
|
||||||
|
|
||||||
|
// remember the currently-bound shaders
|
||||||
|
void on_set_vertex_shader(IDirect3DVertexShader9 *shader);
|
||||||
|
void on_set_pixel_shader(IDirect3DPixelShader9 *shader);
|
||||||
|
|
||||||
|
// true if the current draw call should be dropped (skip active AND the bound
|
||||||
|
// shaders identify it as SDVX Live2D)
|
||||||
|
bool should_skip_draw();
|
||||||
|
|
||||||
|
#else // !SPICE64
|
||||||
|
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point
|
||||||
|
// compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their
|
||||||
|
// call sites.
|
||||||
|
inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {}
|
||||||
|
inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {}
|
||||||
|
inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {}
|
||||||
|
inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {}
|
||||||
|
inline bool should_skip_draw() { return false; }
|
||||||
|
|
||||||
|
#endif // SPICE64
|
||||||
|
}
|
||||||
@@ -86,6 +86,10 @@ static bool monitor_layout_needs_reset = false;
|
|||||||
bool GRAPHICS_CAPTURE_CURSOR = false;
|
bool GRAPHICS_CAPTURE_CURSOR = false;
|
||||||
bool GRAPHICS_LOG_HRESULT = false;
|
bool GRAPHICS_LOG_HRESULT = false;
|
||||||
bool GRAPHICS_SDVX_FORCE_720 = false;
|
bool GRAPHICS_SDVX_FORCE_720 = false;
|
||||||
|
#ifdef SPICE64
|
||||||
|
SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Off;
|
||||||
|
std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY = false;
|
||||||
|
#endif // SPICE64
|
||||||
bool GRAPHICS_SHOW_CURSOR = false;
|
bool GRAPHICS_SHOW_CURSOR = false;
|
||||||
bool GRAPHICS_WINDOWED = false;
|
bool GRAPHICS_WINDOWED = false;
|
||||||
std::vector<HWND> GRAPHICS_WINDOWS;
|
std::vector<HWND> GRAPHICS_WINDOWS;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -27,6 +28,33 @@ enum graphics_dx9on12_state {
|
|||||||
DX9ON12_FORCE_ON,
|
DX9ON12_FORCE_ON,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SDVX Live2D suppression policy. the mode comes from the launcher
|
||||||
|
// option; the in-gameplay flag is maintained by the SDVX scene-detection hook in
|
||||||
|
// games/sdvx (which does not require the SDVX game module to be enabled). the
|
||||||
|
// d3d9 backend reads graphics_sdvx_live2d_should_skip() on every draw.
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
|
||||||
|
// compiled out of 32-bit builds.
|
||||||
|
#ifdef SPICE64
|
||||||
|
enum class SdvxLive2dMode {
|
||||||
|
Off, // leave Live2D untouched (default)
|
||||||
|
Always, // always skip Live2D draws (also removes the menu navigator)
|
||||||
|
InGame, // skip Live2D draws only during a song
|
||||||
|
};
|
||||||
|
extern SdvxLive2dMode GRAPHICS_SDVX_LIVE2D_MODE;
|
||||||
|
extern std::atomic<bool> GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY;
|
||||||
|
|
||||||
|
inline bool graphics_sdvx_live2d_should_skip() {
|
||||||
|
switch (GRAPHICS_SDVX_LIVE2D_MODE) {
|
||||||
|
case SdvxLive2dMode::Always:
|
||||||
|
return true;
|
||||||
|
case SdvxLive2dMode::InGame:
|
||||||
|
return GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.load(std::memory_order_relaxed);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // SPICE64
|
||||||
|
|
||||||
// flag settings
|
// flag settings
|
||||||
extern bool GRAPHICS_CAPTURE_CURSOR;
|
extern bool GRAPHICS_CAPTURE_CURSOR;
|
||||||
extern bool GRAPHICS_LOG_HRESULT;
|
extern bool GRAPHICS_LOG_HRESULT;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
#include "games/sc/sc.h"
|
#include "games/sc/sc.h"
|
||||||
#include "games/scotto/scotto.h"
|
#include "games/scotto/scotto.h"
|
||||||
#include "games/sdvx/sdvx.h"
|
#include "games/sdvx/sdvx.h"
|
||||||
|
#include "games/sdvx/sdvx_live2d.h"
|
||||||
#include "games/shared/printer.h"
|
#include "games/shared/printer.h"
|
||||||
#include "games/silentscope/silentscope.h"
|
#include "games/silentscope/silentscope.h"
|
||||||
#include "games/mfc/mfc.h"
|
#include "games/mfc/mfc.h"
|
||||||
@@ -102,6 +103,7 @@
|
|||||||
#include "overlay/notifications.h"
|
#include "overlay/notifications.h"
|
||||||
#include "overlay/windows/patch_manager.h"
|
#include "overlay/windows/patch_manager.h"
|
||||||
#include "overlay/windows/iidx_seg.h"
|
#include "overlay/windows/iidx_seg.h"
|
||||||
|
#include "overlay/windows/obs.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "rawinput/touch.h"
|
#include "rawinput/touch.h"
|
||||||
#include "reader/reader.h"
|
#include "reader/reader.h"
|
||||||
@@ -643,6 +645,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
|
if (options[launcher::Options::PopnNativeTouch].value_bool()) {
|
||||||
games::popn::NATIVE_TOUCH = true;
|
games::popn::NATIVE_TOUCH = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::PopnSubRedraw].value_bool()) {
|
||||||
|
SUBSCREEN_FORCE_REDRAW = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) {
|
if (options[launcher::Options::LoadMetalGearArcadeModule].value_bool()) {
|
||||||
attach_mga = true;
|
attach_mga = true;
|
||||||
}
|
}
|
||||||
@@ -1042,6 +1047,20 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::SDVXForce720p].value_bool()) {
|
if (options[launcher::Options::SDVXForce720p].value_bool()) {
|
||||||
GRAPHICS_SDVX_FORCE_720 = true;
|
GRAPHICS_SDVX_FORCE_720 = true;
|
||||||
}
|
}
|
||||||
|
#ifdef SPICE64
|
||||||
|
// only the Live2D-capable SDVX versions are 64-bit, so this whole feature is
|
||||||
|
// gated out of 32-bit builds and only armed for SDVX (model KFC).
|
||||||
|
if (avs::game::is_model("KFC")) {
|
||||||
|
auto live2d = options[launcher::Options::SDVXDisableLive2D].value_text();
|
||||||
|
if (live2d == "always") {
|
||||||
|
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::Always;
|
||||||
|
} else if (live2d == "ingame") {
|
||||||
|
GRAPHICS_SDVX_LIVE2D_MODE = SdvxLive2dMode::InGame;
|
||||||
|
// scene detection runs independently of the SDVX game module
|
||||||
|
games::sdvx::live2d_scene_detection_init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // SPICE64
|
||||||
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
if (options[launcher::Options::InvertTouchCoordinates].value_bool()) {
|
||||||
rawinput::touch::INVERTED = true;
|
rawinput::touch::INVERTED = true;
|
||||||
}
|
}
|
||||||
@@ -1434,6 +1453,26 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
|
cfg::CONFIGURATOR_FORCE_SOFTWARE_RENDER = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OBS WebSocket overlay settings
|
||||||
|
if (options[launcher::Options::OBSWebSocketEnabled].value_bool()) {
|
||||||
|
overlay::windows::OBS_CONTROL_ENABLED = true;
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketHost].is_active()) {
|
||||||
|
overlay::windows::OBS_CONTROL_HOST = options[launcher::Options::OBSWebSocketHost].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketPort].is_active()) {
|
||||||
|
const auto obs_port = options[launcher::Options::OBSWebSocketPort].value_uint32();
|
||||||
|
if (obs_port > 0 && obs_port <= 65535) {
|
||||||
|
overlay::windows::OBS_CONTROL_PORT = static_cast<uint16_t>(obs_port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketPassword].is_active()) {
|
||||||
|
overlay::windows::OBS_CONTROL_PASSWORD = options[launcher::Options::OBSWebSocketPassword].value_text();
|
||||||
|
}
|
||||||
|
if (options[launcher::Options::OBSWebSocketDebug].value_bool()) {
|
||||||
|
overlay::windows::OBS_CONTROL_DEBUG = true;
|
||||||
|
}
|
||||||
|
|
||||||
// API debugging
|
// API debugging
|
||||||
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
|
if (api_debug && !cfg::CONFIGURATOR_STANDALONE) {
|
||||||
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
|
API_CONTROLLER = std::make_unique<api::Controller>(api_port, api_pass, api_pretty);
|
||||||
@@ -1546,8 +1585,19 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
|
|
||||||
// print out conflicts
|
// print out conflicts
|
||||||
size_t conflicts = 0;
|
size_t conflicts = 0;
|
||||||
for (const auto &option : options) {
|
for (size_t i = 0; i < options.size(); i++) {
|
||||||
if (option.conflicting && option.get_definition().type != OptionType::Bool) {
|
// InjectHook / EarlyInjectHook accept multiple values, so command line and
|
||||||
|
// spicecfg entries are merged rather than conflicting; don't warn about them
|
||||||
|
if (i == (size_t) launcher::Options::InjectHook ||
|
||||||
|
i == (size_t) launcher::Options::EarlyInjectHook) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &option = options[i];
|
||||||
|
// ignore Boolean values
|
||||||
|
if (option.get_definition().type == OptionType::Bool) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (option.conflicting) {
|
||||||
conflicts += 1;
|
conflicts += 1;
|
||||||
const auto& value = option.get_definition().sensitive ? "*****" : option.value;
|
const auto& value = option.get_definition().sensitive ? "*****" : option.value;
|
||||||
if (launcher::USE_CMD_OVERRIDE) {
|
if (launcher::USE_CMD_OVERRIDE) {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ static const std::vector<std::string> CATEGORY_ORDER_NETWORK = {
|
|||||||
static const std::vector<std::string> CATEGORY_ORDER_OVERLAY = {
|
static const std::vector<std::string> CATEGORY_ORDER_OVERLAY = {
|
||||||
"General Overlay",
|
"General Overlay",
|
||||||
"Game Overlay",
|
"Game Overlay",
|
||||||
|
"OBS Control",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::vector<std::string> CATEGORY_ORDER_ADVANCED = {
|
static const std::vector<std::string> CATEGORY_ORDER_ADVANCED = {
|
||||||
@@ -1025,6 +1026,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.quick_setting_category = "Game",
|
.quick_setting_category = "Game",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// SDVXDisableLive2D
|
||||||
|
.title = "SDVX Disable Live2D (EXPERIMENTAL)",
|
||||||
|
.name = "sdvxnolive2d",
|
||||||
|
.desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n"
|
||||||
|
"Off: leave Live2D as-is.\n"
|
||||||
|
"Always: never render Live2D (also removes the menu navigator).\n"
|
||||||
|
"During songs: only skip Live2D during a song; keeps the menu navigator. Scene detection relies on game logging.",
|
||||||
|
.type = OptionType::Enum,
|
||||||
|
.game_name = "Sound Voltex",
|
||||||
|
.category = "Advanced Game Options",
|
||||||
|
.elements = {
|
||||||
|
{"off", "Show Live2D"},
|
||||||
|
{"always", "Never show Live2D"},
|
||||||
|
{"ingame", "Show navigators only"},
|
||||||
|
},
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_SDVXSubPos
|
// spice2x_SDVXSubPos
|
||||||
.title = "SDVX Subscreen Overlay Position",
|
.title = "SDVX Subscreen Overlay Position",
|
||||||
@@ -1138,6 +1157,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.game_name = "Pop'n Music",
|
.game_name = "Pop'n Music",
|
||||||
.category = "Advanced Game Options",
|
.category = "Advanced Game Options",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// PopnSubRedraw
|
||||||
|
.title = "Pop'n Music PikaPika Subscreen Force Redraw",
|
||||||
|
.name = "popnsubredraw",
|
||||||
|
.desc = "Check if submonitor in fullscreen mode appears stuck; "
|
||||||
|
"this option forces subscreen to redraw every frame.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "Pop'n Music",
|
||||||
|
.category = "Advanced Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "Force Load HELLO! Pop'n Music Module",
|
.title = "Force Load HELLO! Pop'n Music Module",
|
||||||
.name = "hpm",
|
.name = "hpm",
|
||||||
@@ -3190,6 +3220,51 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Development",
|
.category = "Development",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketEnabled
|
||||||
|
.title = "OBS WebSocket Enable",
|
||||||
|
.name = "obsenable",
|
||||||
|
.desc = "Enables the in-game OBS Control overlay and its connection to the OBS Studio "
|
||||||
|
"obs-websocket (v5) server.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketHost
|
||||||
|
.title = "OBS WebSocket Host",
|
||||||
|
.name = "obshost",
|
||||||
|
.desc = "Host name or IP address of the OBS Studio obs-websocket (v5) server used by "
|
||||||
|
"the in-game OBS Control overlay. Defaults to 127.0.0.1 when left empty.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketPort
|
||||||
|
.title = "OBS WebSocket Port",
|
||||||
|
.name = "obsport",
|
||||||
|
.desc = "Port of the OBS Studio obs-websocket (v5) server. Defaults to 4455 when left empty.",
|
||||||
|
.type = OptionType::Integer,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketPassword
|
||||||
|
.title = "OBS WebSocket Password",
|
||||||
|
.name = "obspass",
|
||||||
|
.desc = "Password for the OBS Studio obs-websocket (v5) server. Leave empty if "
|
||||||
|
"authentication is disabled in OBS.",
|
||||||
|
.type = OptionType::Text,
|
||||||
|
.category = "OBS Control",
|
||||||
|
.sensitive = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// OBSWebSocketDebug
|
||||||
|
.title = "OBS WebSocket Debug",
|
||||||
|
.name = "obsdebug",
|
||||||
|
.desc = "Writes the OBS WebSocket client's internal connection diagnostics to the log. "
|
||||||
|
"Only enable this when troubleshooting connection problems.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.category = "OBS Control",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ namespace launcher {
|
|||||||
SDVXDigitalKnobSocd,
|
SDVXDigitalKnobSocd,
|
||||||
spice2x_SDVXAsioDriver,
|
spice2x_SDVXAsioDriver,
|
||||||
SDVXAsioTwoChannel,
|
SDVXAsioTwoChannel,
|
||||||
|
SDVXDisableLive2D,
|
||||||
spice2x_SDVXSubPos,
|
spice2x_SDVXSubPos,
|
||||||
SDVXSubMonitorOverride,
|
SDVXSubMonitorOverride,
|
||||||
LoadDDRModule,
|
LoadDDRModule,
|
||||||
@@ -108,6 +109,7 @@ namespace launcher {
|
|||||||
PopnNoSub,
|
PopnNoSub,
|
||||||
PopnSubMonitorOverride,
|
PopnSubMonitorOverride,
|
||||||
PopnNativeTouch,
|
PopnNativeTouch,
|
||||||
|
PopnSubRedraw,
|
||||||
LoadHelloPopnMusicModule,
|
LoadHelloPopnMusicModule,
|
||||||
LoadGitaDoraModule,
|
LoadGitaDoraModule,
|
||||||
GitaDoraTwoChannelAudio,
|
GitaDoraTwoChannelAudio,
|
||||||
@@ -304,7 +306,12 @@ namespace launcher {
|
|||||||
DisableHighResTimer,
|
DisableHighResTimer,
|
||||||
EnableICMPHook,
|
EnableICMPHook,
|
||||||
AutoElevate,
|
AutoElevate,
|
||||||
CfgForceSoftwareRender
|
CfgForceSoftwareRender,
|
||||||
|
OBSWebSocketEnabled,
|
||||||
|
OBSWebSocketHost,
|
||||||
|
OBSWebSocketPort,
|
||||||
|
OBSWebSocketPassword,
|
||||||
|
OBSWebSocketDebug
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class OptionsCategory {
|
enum class OptionsCategory {
|
||||||
|
|||||||
@@ -1327,6 +1327,28 @@ 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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
easywsclient (MIT)
|
||||||
|
-------------------------------------------
|
||||||
|
Copyright (c) 2012 Dhruv Matani, Daniel Baird
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
Contributions
|
Contributions
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
cardio - Felix - MIT License
|
cardio - Felix - MIT License
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "extensions.h"
|
#include "extensions.h"
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "external/imgui/imgui.h"
|
#include "external/imgui/imgui.h"
|
||||||
|
#include "external/imgui/imgui_internal.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
|
||||||
|
|
||||||
@@ -160,6 +162,19 @@ namespace ImGui {
|
|||||||
return clicked;
|
return clicked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ColoredButton(const char* label, const ImVec4& base, const ImVec2& size) {
|
||||||
|
const auto brighten = [](const ImVec4& c, float d) {
|
||||||
|
return ImVec4((std::min)(c.x + d, 1.0f), (std::min)(c.y + d, 1.0f),
|
||||||
|
(std::min)(c.z + d, 1.0f), c.w);
|
||||||
|
};
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Button, base);
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, brighten(base, 0.12f));
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, brighten(base, 0.22f));
|
||||||
|
const bool clicked = ImGui::Button(label, size);
|
||||||
|
ImGui::PopStyleColor(3);
|
||||||
|
return clicked;
|
||||||
|
}
|
||||||
|
|
||||||
bool ClearButton(const std::string& tooltip) {
|
bool ClearButton(const std::string& tooltip) {
|
||||||
ImGui::PushID(tooltip.c_str());
|
ImGui::PushID(tooltip.c_str());
|
||||||
// same colors as a checkbox
|
// same colors as a checkbox
|
||||||
@@ -209,17 +224,17 @@ namespace ImGui {
|
|||||||
return open;
|
return open;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InvisibleTableRowSelectable() {
|
void HighlightTableRowOnHover() {
|
||||||
ImGui::TableSetColumnIndex(0);
|
// hit-test the row rect directly so the row layout and height are untouched
|
||||||
ImGui::PushStyleColor(ImGuiCol_Header, 0);
|
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||||
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, 0);
|
if (table == nullptr) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_HeaderActive, 0);
|
return;
|
||||||
ImGui::PushTabStop(false); // prevent tab navigation
|
}
|
||||||
ImGui::Selectable("##row", false,
|
if (ImGui::IsWindowHovered() &&
|
||||||
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap);
|
ImGui::IsMouseHoveringRect(
|
||||||
ImGui::PopTabStop();
|
ImVec2(table->WorkRect.Min.x, table->RowPosY1),
|
||||||
ImGui::PopStyleColor(3);
|
ImVec2(table->WorkRect.Max.x, table->RowPosY2),
|
||||||
if (ImGui::IsItemHovered()) {
|
false)) {
|
||||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24));
|
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, IM_COL32(200, 200, 200, 24));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ namespace ImGui {
|
|||||||
void TextTruncated(const std::string& p_text, float p_truncated_width);
|
void TextTruncated(const std::string& p_text, float p_truncated_width);
|
||||||
bool DeleteButton(const std::string& tooltip);
|
bool DeleteButton(const std::string& tooltip);
|
||||||
bool ClearButton(const std::string& tooltip);
|
bool ClearButton(const std::string& tooltip);
|
||||||
void InvisibleTableRowSelectable();
|
void HighlightTableRowOnHover();
|
||||||
|
|
||||||
|
// a Button with the given base fill color; the hovered/active shades are
|
||||||
|
// derived by brightening the base. size defaults to auto (fit the label).
|
||||||
|
bool ColoredButton(const char* label, const ImVec4& base,
|
||||||
|
const ImVec2& size = ImVec2(0, 0));
|
||||||
|
|
||||||
// Config tab bar with extra label padding and uniform, centered tab widths.
|
// Config tab bar with extra label padding and uniform, centered tab widths.
|
||||||
// Wrap items in BeginPaddedTabItem between BeginPaddedTabBar/EndTabBar.
|
// Wrap items in BeginPaddedTabItem between BeginPaddedTabBar/EndTabBar.
|
||||||
|
|||||||
@@ -210,7 +210,15 @@ bool ImGui_ImplSpice_UpdateMouseCursor() {
|
|||||||
|
|
||||||
// update cursor
|
// update cursor
|
||||||
ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
|
ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor();
|
||||||
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) {
|
|
||||||
|
// in auto-hide mode imgui owns cursor drawing, so the OS cursor must stay
|
||||||
|
// hidden. this function is also called from the game window's WM_SETCURSOR
|
||||||
|
// handler, which fires on every mouse move; without forcing it hidden here,
|
||||||
|
// the else branch below would set IDC_ARROW on the next mouse move once the
|
||||||
|
// overlay is closed. that arrow is conspicuous on games whose window class
|
||||||
|
// has hCursor==NULL (e.g. DDR), since those normally show no client-area
|
||||||
|
// cursor at all.
|
||||||
|
if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor || g_MouseCursorAutoHide) {
|
||||||
|
|
||||||
// hide OS mouse cursor if imgui is drawing it or if it wants no cursor
|
// hide OS mouse cursor if imgui is drawing it or if it wants no cursor
|
||||||
::SetCursor(nullptr);
|
::SetCursor(nullptr);
|
||||||
@@ -592,8 +600,12 @@ void ImGui_ImplSpice_NewFrame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// update OS mouse cursor with the cursor requested by imgui
|
// in auto-hide mode imgui owns the cursor, so keep the OS cursor hidden
|
||||||
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor();
|
// even when the overlay is closed (io.MouseDrawCursor is false then).
|
||||||
|
ImGuiMouseCursor mouse_cursor =
|
||||||
|
(io.MouseDrawCursor || g_MouseCursorAutoHide)
|
||||||
|
? ImGuiMouseCursor_None
|
||||||
|
: ImGui::GetMouseCursor();
|
||||||
if (g_LastMouseCursor != mouse_cursor) {
|
if (g_LastMouseCursor != mouse_cursor) {
|
||||||
g_LastMouseCursor = mouse_cursor;
|
g_LastMouseCursor = mouse_cursor;
|
||||||
ImGui_ImplSpice_UpdateMouseCursor();
|
ImGui_ImplSpice_UpdateMouseCursor();
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
#include "windows/sdvx_sub.h"
|
#include "windows/sdvx_sub.h"
|
||||||
#include "windows/keypad.h"
|
#include "windows/keypad.h"
|
||||||
#include "windows/log.h"
|
#include "windows/log.h"
|
||||||
|
#include "windows/obs.h"
|
||||||
#include "windows/patch_manager.h"
|
#include "windows/patch_manager.h"
|
||||||
#include "windows/exitprompt.cpp"
|
#include "windows/exitprompt.cpp"
|
||||||
|
|
||||||
@@ -416,6 +417,12 @@ void overlay::SpiceOverlay::init() {
|
|||||||
}
|
}
|
||||||
this->window_add(new overlay::windows::PatchManager(this));
|
this->window_add(new overlay::windows::PatchManager(this));
|
||||||
|
|
||||||
|
// OBS control spawns a background WebSocket worker; skip it in the standalone
|
||||||
|
// configurator, where there is no running game to stream/record
|
||||||
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
this->window_add(window_obs = new overlay::windows::OBSControl(this));
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
window_keypad1 = new overlay::windows::Keypad(this, 0);
|
window_keypad1 = new overlay::windows::Keypad(this, 0);
|
||||||
this->window_add(window_keypad1);
|
this->window_add(window_keypad1);
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ namespace overlay {
|
|||||||
Window *window_camera = nullptr;
|
Window *window_camera = nullptr;
|
||||||
Window *window_sub = nullptr;
|
Window *window_sub = nullptr;
|
||||||
Window *window_log = nullptr;
|
Window *window_log = nullptr;
|
||||||
|
Window *window_obs = nullptr;
|
||||||
|
|
||||||
// not part of `windows`: drawn/updated on the persistent layer (like
|
// not part of `windows`: drawn/updated on the persistent layer (like
|
||||||
// notifications), independent of the overlay's active state and input gates.
|
// notifications), independent of the overlay's active state and input gates.
|
||||||
|
|||||||
@@ -928,18 +928,14 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
// primary
|
// primary
|
||||||
build_button(name, primary_button, &primary_button, button_it, button_it_max, 0);
|
build_button(name, primary_button, &primary_button, button_it, button_it_max, 0);
|
||||||
ImGui::PushID(&primary_button);
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::InvisibleTableRowSelectable();
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
// alternatives
|
// alternatives
|
||||||
int alt_index = 1; // 0 is primary
|
int alt_index = 1; // 0 is primary
|
||||||
for (auto &alt : primary_button.getAlternatives()) {
|
for (auto &alt : primary_button.getAlternatives()) {
|
||||||
if (alt.isValid()) {
|
if (alt.isValid()) {
|
||||||
build_button(name, primary_button, &alt, button_it, button_it_max, alt_index);
|
build_button(name, primary_button, &alt, button_it, button_it_max, alt_index);
|
||||||
ImGui::PushID(&alt);
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::InvisibleTableRowSelectable();
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
}
|
||||||
alt_index++;
|
alt_index++;
|
||||||
}
|
}
|
||||||
@@ -2519,8 +2515,7 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
edit_analog_popup(analog, title);
|
edit_analog_popup(analog, title);
|
||||||
|
|
||||||
// row hover detection (invisible selectable that spans entire row)
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::InvisibleTableRowSelectable();
|
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -3174,16 +3169,12 @@ namespace overlay::windows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
build_light(light, &light, i, 0);
|
build_light(light, &light, i, 0);
|
||||||
ImGui::PushID(&light);
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::InvisibleTableRowSelectable();
|
|
||||||
ImGui::PopID();
|
|
||||||
int alt_index = 1;
|
int alt_index = 1;
|
||||||
for (auto &alt : light.getAlternatives()) {
|
for (auto &alt : light.getAlternatives()) {
|
||||||
if (alt.isValid()) {
|
if (alt.isValid()) {
|
||||||
build_light(light, &alt, i, alt_index);
|
build_light(light, &alt, i, alt_index);
|
||||||
ImGui::PushID(&alt);
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::InvisibleTableRowSelectable();
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
}
|
||||||
alt_index++;
|
alt_index++;
|
||||||
}
|
}
|
||||||
@@ -4209,8 +4200,10 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card overrides");
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card overrides");
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextUnformatted(
|
ImGui::TextWrapped(
|
||||||
"Specify hardcoded card numbers here. This will always take priority when Insert Card is pressed.");
|
"%s",
|
||||||
|
"Specify hardcoded card numbers here.\n\n"
|
||||||
|
"Overrides will always take priority when Insert Card is pressed.");
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
// read in values from options
|
// read in values from options
|
||||||
@@ -4387,7 +4380,8 @@ namespace overlay::windows {
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card from text files");
|
ImGui::TextColored(ImVec4(1, 0.7f, 0, 1), "Card from text files");
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::TextUnformatted(
|
ImGui::TextWrapped(
|
||||||
|
"%s",
|
||||||
"Use text files on disk; its content will be read when Insert Card is pressed.");
|
"Use text files on disk; its content will be read when Insert Card is pressed.");
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
|
|
||||||
@@ -5293,8 +5287,7 @@ namespace overlay::windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// row hover detection (invisible selectable that spans entire row)
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::InvisibleTableRowSelectable();
|
|
||||||
|
|
||||||
// next item
|
// next item
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
@@ -5787,23 +5780,28 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
// name
|
// name
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextTruncated(
|
ImGui::TextTruncated(
|
||||||
t.name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
|
t.name, ImGui::GetContentRegionAvail().x - overlay::apply_scaling(20));
|
||||||
|
|
||||||
// type
|
// type
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::TextUnformatted(t.is_builtin ? "Built-in" : "User");
|
ImGui::TextUnformatted(t.is_builtin ? "Built-in" : "User");
|
||||||
|
|
||||||
// buttons
|
// buttons
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::Text("%d", (int)t.buttons.size());
|
ImGui::Text("%d", (int)t.buttons.size());
|
||||||
|
|
||||||
// analogs
|
// analogs
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::Text("%d", (int)t.analogs.size());
|
ImGui::Text("%d", (int)t.analogs.size());
|
||||||
|
|
||||||
// lights
|
// lights
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::Text("%d", (int)t.lights.size());
|
ImGui::Text("%d", (int)t.lights.size());
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
@@ -5831,7 +5829,7 @@ namespace overlay::windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::InvisibleTableRowSelectable();
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,8 +119,9 @@ namespace overlay::windows {
|
|||||||
ImGui::TextDisabled("Graphics");
|
ImGui::TextDisabled("Graphics");
|
||||||
build_button(this->overlay->window_camera, "Camera control", size, NextItem::NEW_LINE);
|
build_button(this->overlay->window_camera, "Camera control", size, NextItem::NEW_LINE);
|
||||||
|
|
||||||
build_button(this->overlay->window_fps.get(), "FPS", size_half, NextItem::SAME_LINE);
|
build_button(this->overlay->window_fps.get(), "FPS", size_third, NextItem::SAME_LINE);
|
||||||
build_button(this->overlay->window_resize, "Resize", size_half, NextItem::NEW_LINE);
|
build_button(this->overlay->window_obs, "OBS", size_third, NextItem::SAME_LINE);
|
||||||
|
build_button(this->overlay->window_resize, "Resize", size_third, NextItem::NEW_LINE);
|
||||||
|
|
||||||
ImGui::TextDisabled("I/O");
|
ImGui::TextDisabled("I/O");
|
||||||
build_button(this->overlay->window_cards, "Card Manager", size, NextItem::NEW_LINE);
|
build_button(this->overlay->window_cards, "Card Manager", size, NextItem::NEW_LINE);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "external/fmt/include/fmt/chrono.h"
|
#include "external/fmt/include/fmt/chrono.h"
|
||||||
#include "fps.h"
|
#include "fps.h"
|
||||||
|
#include "obs.h"
|
||||||
|
|
||||||
namespace overlay::windows {
|
namespace overlay::windows {
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ namespace overlay::windows {
|
|||||||
this->title = "Stats";
|
this->title = "Stats";
|
||||||
this->flags = ImGuiWindowFlags_NoTitleBar
|
this->flags = ImGuiWindowFlags_NoTitleBar
|
||||||
| ImGuiWindowFlags_NoResize
|
| ImGuiWindowFlags_NoResize
|
||||||
|
| ImGuiWindowFlags_AlwaysAutoResize
|
||||||
| ImGuiWindowFlags_NoCollapse
|
| ImGuiWindowFlags_NoCollapse
|
||||||
| ImGuiWindowFlags_NoFocusOnAppearing
|
| ImGuiWindowFlags_NoFocusOnAppearing
|
||||||
| ImGuiWindowFlags_NoBringToFrontOnFocus
|
| ImGuiWindowFlags_NoBringToFrontOnFocus
|
||||||
@@ -29,25 +31,7 @@ namespace overlay::windows {
|
|||||||
std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
|
std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FPS::calculate_initial_window() {
|
ImVec2 FPS::anchored_pos(const ImVec2 &size) const {
|
||||||
// size the window explicitly (no AlwaysAutoResize) so the corner anchoring
|
|
||||||
// below is exact; the footprint mirrors the fixed-fit table in build_content()
|
|
||||||
const float line_h = ImGui::GetTextLineHeight();
|
|
||||||
const int rows = 3;
|
|
||||||
|
|
||||||
// widest label and widest value drive the two fixed-fit columns
|
|
||||||
const float label_w = (std::max)(
|
|
||||||
ImGui::CalcTextSize("Time").x,
|
|
||||||
ImGui::CalcTextSize("Game").x);
|
|
||||||
const float value_w = ImGui::CalcTextSize("00:00:00").x;
|
|
||||||
|
|
||||||
const float win_w = label_w + value_w
|
|
||||||
+ FPS_CELL_PADDING.x * 2
|
|
||||||
+ FPS_WINDOW_PADDING.x * 2;
|
|
||||||
const float win_h = (line_h + FPS_CELL_PADDING.y * 2) * rows
|
|
||||||
+ FPS_WINDOW_PADDING.y * 2;
|
|
||||||
this->init_size = ImVec2(win_w, win_h);
|
|
||||||
|
|
||||||
// bottom-anchored windows use a larger edge margin (matching notification
|
// bottom-anchored windows use a larger edge margin (matching notification
|
||||||
// toasts) since they overlap the same on-screen UI; other edges hug closer
|
// toasts) since they overlap the same on-screen UI; other edges hug closer
|
||||||
const float edge_margin = overlay::apply_scaling(4);
|
const float edge_margin = overlay::apply_scaling(4);
|
||||||
@@ -61,9 +45,27 @@ namespace overlay::windows {
|
|||||||
overlay::FPS_LOCATION == overlay::FpsLocation::BottomLeft ||
|
overlay::FPS_LOCATION == overlay::FpsLocation::BottomLeft ||
|
||||||
overlay::FPS_LOCATION == overlay::FpsLocation::BottomRight;
|
overlay::FPS_LOCATION == overlay::FpsLocation::BottomRight;
|
||||||
|
|
||||||
const float pos_x = right ? display.x - win_w - edge_margin : edge_margin;
|
const float pos_x = right ? display.x - size.x - edge_margin : edge_margin;
|
||||||
const float pos_y = bottom ? display.y - win_h - bottom_margin : edge_margin;
|
const float pos_y = bottom ? display.y - size.y - bottom_margin : edge_margin;
|
||||||
this->init_pos = ImVec2(pos_x, pos_y);
|
return ImVec2(pos_x, pos_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FPS::calculate_initial_window() {
|
||||||
|
// first-frame size estimate for the base 3 rows; AlwaysAutoResize handles
|
||||||
|
// the exact size (incl. any OBS rows) and build_content re-anchors each frame
|
||||||
|
const float line_h = ImGui::GetTextLineHeight();
|
||||||
|
const float label_w = (std::max)(
|
||||||
|
ImGui::CalcTextSize("Time").x,
|
||||||
|
ImGui::CalcTextSize("Game").x);
|
||||||
|
const float value_w = ImGui::CalcTextSize("00:00:00").x;
|
||||||
|
|
||||||
|
const float win_w = label_w + value_w
|
||||||
|
+ FPS_CELL_PADDING.x * 2
|
||||||
|
+ FPS_WINDOW_PADDING.x * 2;
|
||||||
|
const float win_h = (line_h + FPS_CELL_PADDING.y * 2) * 3
|
||||||
|
+ FPS_WINDOW_PADDING.y * 2;
|
||||||
|
this->init_size = ImVec2(win_w, win_h);
|
||||||
|
this->init_pos = this->anchored_pos(this->init_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FPS::build_content() {
|
void FPS::build_content() {
|
||||||
@@ -79,6 +81,21 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
const auto uptime = now_s - this->start_time;
|
const auto uptime = now_s - this->start_time;
|
||||||
|
|
||||||
|
// OBS status (only adds rows while streaming live or recording/paused)
|
||||||
|
OBSStatus obs_status;
|
||||||
|
bool show_stream = false;
|
||||||
|
bool show_record = false;
|
||||||
|
if (auto *obs = static_cast<OBSControl *>(this->overlay->window_obs)) {
|
||||||
|
obs_status = obs->get_status();
|
||||||
|
show_stream = obs_status.streaming;
|
||||||
|
show_record = obs_status.recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlwaysAutoResize sizes the window to its content, so adding/removing OBS
|
||||||
|
// rows never clips; just re-anchor it to the configured corner each frame
|
||||||
|
// using the actual (auto-sized) dimensions
|
||||||
|
ImGui::SetWindowPos(this->anchored_pos(ImGui::GetWindowSize()), ImGuiCond_Always);
|
||||||
|
|
||||||
// right-align a label within the current cell so the label column reads
|
// right-align a label within the current cell so the label column reads
|
||||||
// flush against the value column instead of looking ragged. the label is
|
// flush against the value column instead of looking ragged. the label is
|
||||||
// only slightly dimmer than normal text (not the much darker "disabled" tone)
|
// only slightly dimmer than normal text (not the much darker "disabled" tone)
|
||||||
@@ -117,6 +134,31 @@ namespace overlay::windows {
|
|||||||
fmt::format("{:%H:%M:%S}",
|
fmt::format("{:%H:%M:%S}",
|
||||||
std::chrono::floor<std::chrono::seconds>(uptime)).c_str());
|
std::chrono::floor<std::chrono::seconds>(uptime)).c_str());
|
||||||
|
|
||||||
|
// OBS rows - only present while live or recording
|
||||||
|
const ImVec4 col_red(0.90f, 0.30f, 0.30f, 1.0f);
|
||||||
|
const ImVec4 col_yellow(0.95f, 0.80f, 0.30f, 1.0f);
|
||||||
|
if (show_stream) {
|
||||||
|
const int64_t ms = OBSControl::live_duration_ms(
|
||||||
|
obs_status.stream_duration_ms, obs_status.stream_duration_base_tick, true);
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
label("Live");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::TextColored(col_red, "%s",
|
||||||
|
fmt::format("{:%H:%M:%S}", std::chrono::seconds(ms / 1000)).c_str());
|
||||||
|
}
|
||||||
|
if (show_record) {
|
||||||
|
const int64_t ms = OBSControl::live_duration_ms(
|
||||||
|
obs_status.record_duration_ms, obs_status.record_duration_base_tick,
|
||||||
|
!obs_status.record_paused);
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
label("Rec");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::TextColored(obs_status.record_paused ? col_yellow : col_red, "%s",
|
||||||
|
fmt::format("{:%H:%M:%S}", std::chrono::seconds(ms / 1000)).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar();
|
ImGui::PopStyleVar();
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ namespace overlay::windows {
|
|||||||
private:
|
private:
|
||||||
std::chrono::system_clock::time_point start_time;
|
std::chrono::system_clock::time_point start_time;
|
||||||
|
|
||||||
|
// anchored top-left position for a window of the given size, per FPS_LOCATION
|
||||||
|
ImVec2 anchored_pos(const ImVec2 &size) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
FPS(SpiceOverlay *overlay);
|
FPS(SpiceOverlay *overlay);
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
#include "obs.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "external/imgui/imgui.h"
|
||||||
|
|
||||||
|
#include "games/io.h"
|
||||||
|
#include "overlay/overlay.h"
|
||||||
|
#include "overlay/imgui/extensions.h"
|
||||||
|
|
||||||
|
using namespace std::chrono;
|
||||||
|
|
||||||
|
// OBS WebSocket protocol/worker thread lives in obs_websocket.cpp; this file
|
||||||
|
// owns the ImGui control window and the connection lifecycle.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// status text colors
|
||||||
|
const ImVec4 COL_GREEN(0.40f, 0.85f, 0.40f, 1.0f);
|
||||||
|
const ImVec4 COL_RED(0.90f, 0.30f, 0.30f, 1.0f);
|
||||||
|
const ImVec4 COL_YELLOW(0.95f, 0.80f, 0.30f, 1.0f);
|
||||||
|
const ImVec4 COL_GREY(0.60f, 0.60f, 0.60f, 1.0f);
|
||||||
|
|
||||||
|
// muted action-button fills (start = green, stop = red, pause = yellow); the
|
||||||
|
// hovered/active shades are derived by brightening the base
|
||||||
|
const ImVec4 COL_BTN_GREEN(0.20f, 0.45f, 0.24f, 1.0f);
|
||||||
|
const ImVec4 COL_BTN_RED(0.52f, 0.20f, 0.20f, 1.0f);
|
||||||
|
const ImVec4 COL_BTN_YELLOW(0.52f, 0.42f, 0.16f, 1.0f);
|
||||||
|
|
||||||
|
// an in-flight request lingers for at most this long before the button frees
|
||||||
|
// itself, so a dropped state event can never wedge a control permanently
|
||||||
|
const int64_t PENDING_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
int64_t now_tick_ms() {
|
||||||
|
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string format_duration(int64_t ms) {
|
||||||
|
if (ms < 0) {
|
||||||
|
ms = 0;
|
||||||
|
}
|
||||||
|
const int64_t total_seconds = ms / 1000;
|
||||||
|
const int64_t hours = total_seconds / 3600;
|
||||||
|
const int64_t minutes = (total_seconds % 3600) / 60;
|
||||||
|
const int64_t seconds = total_seconds % 60;
|
||||||
|
char buf[16];
|
||||||
|
snprintf(buf, sizeof(buf), "%02lld:%02lld:%02lld",
|
||||||
|
static_cast<long long>(hours),
|
||||||
|
static_cast<long long>(minutes),
|
||||||
|
static_cast<long long>(seconds));
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace overlay::windows {
|
||||||
|
|
||||||
|
OBSControl::OBSControl(SpiceOverlay *overlay) : Window(overlay) {
|
||||||
|
this->title = "OBS Control";
|
||||||
|
this->flags |= ImGuiWindowFlags_AlwaysAutoResize;
|
||||||
|
this->init_pos = overlay::apply_scaling_to_vector(120, 120);
|
||||||
|
this->toggle_button = games::OverlayButtons::ToggleOBSControl;
|
||||||
|
|
||||||
|
this->worker_running.store(true);
|
||||||
|
this->worker_thread = std::thread(&OBSControl::worker_main, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
OBSControl::~OBSControl() {
|
||||||
|
// signal stop and wake any in-progress interruptible_sleep at once; the
|
||||||
|
// lock around the store pairs with the wait predicate to avoid a lost wakeup
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->worker_mutex);
|
||||||
|
this->worker_running.store(false);
|
||||||
|
}
|
||||||
|
this->worker_cv.notify_all();
|
||||||
|
if (this->worker_thread.joinable()) {
|
||||||
|
// note: if the worker is mid-connect, WebSocket::from_url performs a
|
||||||
|
// blocking getaddrinfo/connect that does not observe worker_running,
|
||||||
|
// so this join can stall for the OS connect timeout. the default
|
||||||
|
// 127.0.0.1 host fails fast (connection refused); only a misconfigured
|
||||||
|
// unreachable remote OBS_CONTROL_HOST would delay shutdown here.
|
||||||
|
this->worker_thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OBSStatus OBSControl::get_status() {
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
return this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t OBSControl::live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking) {
|
||||||
|
if (!ticking) {
|
||||||
|
return (std::max<int64_t>)(base_ms, 0);
|
||||||
|
}
|
||||||
|
const int64_t now =
|
||||||
|
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
||||||
|
// clamp so a stale base tick / clock hiccup can never yield a negative
|
||||||
|
// duration; callers (FPS rows, build_content) format this directly
|
||||||
|
return (std::max<int64_t>)(base_ms + (now - base_tick), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OBSControl::build_content() {
|
||||||
|
|
||||||
|
const OBSStatus s = this->get_status();
|
||||||
|
|
||||||
|
// label + colored value on a single line
|
||||||
|
const auto status_line = [](const char *label, const ImVec4 &col, const char *value) {
|
||||||
|
ImGui::Text("%s", label);
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(col, "%s", value);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!s.connected) {
|
||||||
|
if (s.disabled) {
|
||||||
|
ImGui::TextColored(COL_GREY, "%s", "OBS Control is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s.identifying) {
|
||||||
|
status_line("OBS WebSocket:", COL_YELLOW, "Connecting...");
|
||||||
|
} else {
|
||||||
|
status_line("OBS WebSocket:", COL_GREY, "Not connected");
|
||||||
|
}
|
||||||
|
const std::string url =
|
||||||
|
"ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
|
||||||
|
status_line("Address:", COL_GREY, url.c_str());
|
||||||
|
if (!s.connection_error.empty()) {
|
||||||
|
ImGui::TextColored(COL_RED, "%s", s.connection_error.c_str());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status_line("OBS WebSocket:", COL_GREEN, "Connected");
|
||||||
|
|
||||||
|
// one fixed content width drives the whole panel so it never resizes as
|
||||||
|
// the scene name or button labels change; every row is sized to fit it
|
||||||
|
const float spacing = ImGui::GetStyle().ItemSpacing.x;
|
||||||
|
const float row_w = overlay::apply_scaling(240);
|
||||||
|
|
||||||
|
if (s.current_scene.empty()) {
|
||||||
|
status_line("Scene:", COL_GREY, "(unknown)");
|
||||||
|
} else {
|
||||||
|
ImGui::Text("Scene:");
|
||||||
|
ImGui::SameLine();
|
||||||
|
// truncate to the remaining row width so "Scene:" + value together
|
||||||
|
// never overflow and push the window wider
|
||||||
|
const float label_w = ImGui::CalcTextSize("Scene:").x;
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, COL_GREY);
|
||||||
|
ImGui::TextTruncated(s.current_scene, row_w - label_w - spacing);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
const int64_t now = now_tick_ms();
|
||||||
|
// every button shares one fixed size; two side-by-side fill the row width,
|
||||||
|
// single buttons keep that same size rather than stretching to fill
|
||||||
|
const ImVec2 btn((row_w - spacing) * 0.5f, 0);
|
||||||
|
|
||||||
|
// has OBS reached the state a pending action was waiting for?
|
||||||
|
const auto reached = [&](OBSAction a) {
|
||||||
|
switch (a) {
|
||||||
|
case OBSAction::StreamStart: return s.streaming;
|
||||||
|
case OBSAction::StreamStop: return !s.streaming;
|
||||||
|
case OBSAction::RecordStart: return s.recording;
|
||||||
|
case OBSAction::RecordStop: return !s.recording;
|
||||||
|
case OBSAction::RecordPause: return s.record_paused;
|
||||||
|
case OBSAction::RecordResume: return !s.record_paused;
|
||||||
|
default: return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// drop a pending action once OBS confirms the new state, or once the
|
||||||
|
// safety deadline lapses (so a dropped event can't wedge the button)
|
||||||
|
const auto settle = [&](OBSAction &slot, int64_t deadline) {
|
||||||
|
if (slot != OBSAction::None && (reached(slot) || now >= deadline)) {
|
||||||
|
slot = OBSAction::None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
settle(this->stream_pending, this->stream_pending_deadline);
|
||||||
|
settle(this->record_pending, this->record_pending_deadline);
|
||||||
|
|
||||||
|
// a colored button that fires a request and marks the output busy on click
|
||||||
|
const auto action_button =
|
||||||
|
[&](const char *label,
|
||||||
|
const ImVec4 &color,
|
||||||
|
const char *request,
|
||||||
|
OBSAction &slot,
|
||||||
|
int64_t &deadline,
|
||||||
|
OBSAction action) {
|
||||||
|
|
||||||
|
if (ImGui::ColoredButton(label, color, btn)) {
|
||||||
|
enqueue_request(request);
|
||||||
|
slot = action;
|
||||||
|
deadline = now + PENDING_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// streaming
|
||||||
|
{
|
||||||
|
const bool pending = this->stream_pending != OBSAction::None;
|
||||||
|
|
||||||
|
if (s.streaming) {
|
||||||
|
const int64_t ms = live_duration_ms(
|
||||||
|
s.stream_duration_ms, s.stream_duration_base_tick, true);
|
||||||
|
status_line("Streaming:", COL_RED, ("LIVE " + format_duration(ms)).c_str());
|
||||||
|
} else {
|
||||||
|
status_line("Streaming:", COL_GREY, pending ? "Starting..." : "Idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(pending);
|
||||||
|
if (s.streaming) {
|
||||||
|
action_button(
|
||||||
|
pending ? "Stopping...##stream" : "Stop Streaming##stream",
|
||||||
|
COL_BTN_RED,
|
||||||
|
"StopStream",
|
||||||
|
this->stream_pending,
|
||||||
|
this->stream_pending_deadline,
|
||||||
|
OBSAction::StreamStop);
|
||||||
|
} else {
|
||||||
|
action_button(
|
||||||
|
pending ? "Starting...##stream" : "Start Streaming##stream",
|
||||||
|
COL_BTN_GREEN,
|
||||||
|
"StartStream",
|
||||||
|
this->stream_pending,
|
||||||
|
this->stream_pending_deadline,
|
||||||
|
OBSAction::StreamStart);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
// recording
|
||||||
|
{
|
||||||
|
const bool pending = this->record_pending != OBSAction::None;
|
||||||
|
|
||||||
|
if (!s.recording) {
|
||||||
|
status_line("Recording:", COL_GREY, pending ? "Starting..." : "Idle");
|
||||||
|
ImGui::BeginDisabled(pending);
|
||||||
|
action_button(
|
||||||
|
pending ? "Starting...##record" : "Start Recording##record",
|
||||||
|
COL_BTN_GREEN,
|
||||||
|
"StartRecord",
|
||||||
|
this->record_pending,
|
||||||
|
this->record_pending_deadline,
|
||||||
|
OBSAction::RecordStart);
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int64_t ms = live_duration_ms(
|
||||||
|
s.record_duration_ms, s.record_duration_base_tick, !s.record_paused);
|
||||||
|
if (s.record_paused) {
|
||||||
|
status_line("Recording:", COL_YELLOW, ("PAUSED " + format_duration(ms)).c_str());
|
||||||
|
} else {
|
||||||
|
status_line("Recording:", COL_RED, ("REC " + format_duration(ms)).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(pending);
|
||||||
|
action_button(
|
||||||
|
this->record_pending == OBSAction::RecordStop ? "Stopping...##record" : "Stop Recording##record",
|
||||||
|
COL_BTN_RED,
|
||||||
|
"StopRecord",
|
||||||
|
this->record_pending,
|
||||||
|
this->record_pending_deadline, OBSAction::RecordStop);
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (s.record_paused) {
|
||||||
|
action_button(
|
||||||
|
this->record_pending == OBSAction::RecordResume ? "Resuming...##record_toggle" : "Resume##record_toggle",
|
||||||
|
COL_BTN_GREEN,
|
||||||
|
"ResumeRecord",
|
||||||
|
this->record_pending,
|
||||||
|
this->record_pending_deadline,
|
||||||
|
OBSAction::RecordResume);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
action_button(
|
||||||
|
this->record_pending == OBSAction::RecordPause ? "Pausing...##record_toggle" : "Pause##record_toggle",
|
||||||
|
COL_BTN_YELLOW,
|
||||||
|
"PauseRecord",
|
||||||
|
this->record_pending,
|
||||||
|
this->record_pending_deadline,
|
||||||
|
OBSAction::RecordPause);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "external/rapidjson/fwd.h"
|
||||||
|
#include "overlay/window.h"
|
||||||
|
|
||||||
|
namespace easywsclient {
|
||||||
|
class WebSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace overlay::windows {
|
||||||
|
|
||||||
|
// OBS WebSocket connection settings, resolved once at launch from the merged
|
||||||
|
// launcher options (command line + saved config) following the same pattern
|
||||||
|
// as the other global launch settings in launcher.cpp
|
||||||
|
extern bool OBS_CONTROL_ENABLED;
|
||||||
|
extern std::string OBS_CONTROL_HOST;
|
||||||
|
extern uint16_t OBS_CONTROL_PORT;
|
||||||
|
extern std::string OBS_CONTROL_PASSWORD;
|
||||||
|
|
||||||
|
// when true, easywsclient's internal diagnostics are routed to the logger
|
||||||
|
extern bool OBS_CONTROL_DEBUG;
|
||||||
|
|
||||||
|
// status snapshot shared between the OBS worker thread and the render thread
|
||||||
|
struct OBSStatus {
|
||||||
|
bool disabled = true;
|
||||||
|
bool connected = false;
|
||||||
|
bool identifying = false;
|
||||||
|
std::string connection_error;
|
||||||
|
|
||||||
|
// name of the active program scene (read-only, from obs-websocket)
|
||||||
|
std::string current_scene;
|
||||||
|
|
||||||
|
bool streaming = false;
|
||||||
|
bool recording = false;
|
||||||
|
bool record_paused = false;
|
||||||
|
|
||||||
|
// duration base values (milliseconds) and the local timestamp (ms since
|
||||||
|
// steady epoch) at which they were last refreshed, so the UI can tick a
|
||||||
|
// smooth timer between polls
|
||||||
|
int64_t stream_duration_ms = 0;
|
||||||
|
int64_t record_duration_ms = 0;
|
||||||
|
int64_t stream_duration_base_tick = 0;
|
||||||
|
int64_t record_duration_base_tick = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// in-flight user action used purely for UI feedback: when the user clicks a
|
||||||
|
// control we remember what we asked for so the button can show a transitional
|
||||||
|
// label and stay disabled until the observed OBS state matches the request
|
||||||
|
// (or a short deadline lapses). owned solely by the render thread.
|
||||||
|
enum class OBSAction {
|
||||||
|
None,
|
||||||
|
StreamStart, StreamStop,
|
||||||
|
RecordStart, RecordStop,
|
||||||
|
RecordPause, RecordResume,
|
||||||
|
};
|
||||||
|
|
||||||
|
class OBSControl : public Window {
|
||||||
|
public:
|
||||||
|
OBSControl(SpiceOverlay *overlay);
|
||||||
|
~OBSControl() override;
|
||||||
|
|
||||||
|
void build_content() override;
|
||||||
|
|
||||||
|
// thread-safe snapshot of the current status for external widgets (e.g. FPS)
|
||||||
|
OBSStatus get_status();
|
||||||
|
|
||||||
|
// live (ticked) duration in ms from a base value/tick captured at last poll
|
||||||
|
static int64_t live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// worker thread entry + helpers (implementation owns the WebSocket)
|
||||||
|
void worker_main();
|
||||||
|
|
||||||
|
// run one connected session loop until the socket closes or we stop;
|
||||||
|
// returns true if the obs-websocket handshake reached "Identified", false
|
||||||
|
// if the socket closed first (e.g. OBS rejected our auth)
|
||||||
|
bool run_session(easywsclient::WebSocket *ws, const std::string &password,
|
||||||
|
uint64_t &request_id);
|
||||||
|
|
||||||
|
// handle a single inbound obs-websocket message (parses + dispatches)
|
||||||
|
void handle_message(easywsclient::WebSocket *ws, const std::string &message,
|
||||||
|
const std::string &password, uint64_t &request_id,
|
||||||
|
bool &identified);
|
||||||
|
|
||||||
|
// per-opcode handlers dispatched from handle_message
|
||||||
|
using request_fn = std::function<void(const char *request_type)>;
|
||||||
|
void handle_identified(bool &identified, const request_fn &request);
|
||||||
|
void handle_event(const rapidjson::Value &d, const request_fn &request);
|
||||||
|
void handle_response(const rapidjson::Value &d);
|
||||||
|
|
||||||
|
void enqueue_request(const std::string &request_type);
|
||||||
|
|
||||||
|
// sleep up to total_ms, waking early if the worker is asked to stop
|
||||||
|
void interruptible_sleep(int total_ms);
|
||||||
|
|
||||||
|
// worker thread
|
||||||
|
std::thread worker_thread;
|
||||||
|
std::atomic<bool> worker_running { false };
|
||||||
|
|
||||||
|
// wakes interruptible_sleep immediately when worker_running is cleared,
|
||||||
|
// so shutdown (and the reconnect backoff) never waits out a fixed delay
|
||||||
|
std::mutex worker_mutex;
|
||||||
|
std::condition_variable worker_cv;
|
||||||
|
|
||||||
|
// shared status (guarded by status_mutex)
|
||||||
|
std::mutex status_mutex;
|
||||||
|
OBSStatus status;
|
||||||
|
|
||||||
|
// outgoing user commands (guarded by command_mutex)
|
||||||
|
std::mutex command_mutex;
|
||||||
|
std::deque<std::string> command_queue;
|
||||||
|
|
||||||
|
// transient action feedback, touched only by the render thread (no sync):
|
||||||
|
// remembers the last start/stop/pause request per output so the button can
|
||||||
|
// show a "Starting.../Stopping..." label and stay disabled until OBS reports
|
||||||
|
// the matching state, with *_deadline as a fallback if the update is missed
|
||||||
|
OBSAction stream_pending = OBSAction::None;
|
||||||
|
OBSAction record_pending = OBSAction::None;
|
||||||
|
int64_t stream_pending_deadline = 0;
|
||||||
|
int64_t record_pending_deadline = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
#include <winsock2.h>
|
||||||
|
|
||||||
|
#include "obs.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include "external/easywsclient/easywsclient.hpp"
|
||||||
|
#include "external/rapidjson/document.h"
|
||||||
|
#include "external/rapidjson/stringbuffer.h"
|
||||||
|
#include "external/rapidjson/writer.h"
|
||||||
|
#include "external/hash-library/sha256.h"
|
||||||
|
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
#include "util/crypt.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
// defined in easywsclient.cpp; gates its internal diagnostic output
|
||||||
|
extern bool EASYWSCLIENT_LOGGING_ENABLED;
|
||||||
|
|
||||||
|
using easywsclient::WebSocket;
|
||||||
|
using namespace std::chrono;
|
||||||
|
|
||||||
|
// obs-websocket v5 message flow (https://github.com/obsproject/obs-websocket):
|
||||||
|
// server -> op 0 Hello (may include an auth challenge)
|
||||||
|
// client -> op 1 Identify (answers the challenge, picks rpcVersion)
|
||||||
|
// server -> op 2 Identified (handshake done; requests may now be sent)
|
||||||
|
// server -> op 5 Event (state changes: stream/record/scene/...)
|
||||||
|
// client -> op 6 Request (e.g. GetStreamStatus, StartRecord)
|
||||||
|
// server -> op 7 RequestResponse (reply to a Request, carries responseData)
|
||||||
|
// Every message is { "op": <int>, "d": { ... } }. Event fields are nested under
|
||||||
|
// d["eventData"] and request replies under d["responseData"], not in d directly.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int64_t now_ms() {
|
||||||
|
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
// raw SHA256 digest -> base64 (obs-websocket v5 auth primitive)
|
||||||
|
std::string sha256_base64(const std::string &input) {
|
||||||
|
SHA256 hasher;
|
||||||
|
hasher.add(input.data(), input.size());
|
||||||
|
unsigned char digest[SHA256::HashBytes];
|
||||||
|
hasher.getHash(digest);
|
||||||
|
return crypt::base64_encode(reinterpret_cast<const uint8_t *>(digest), SHA256::HashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth = base64(sha256(base64(sha256(password + salt)) + challenge))
|
||||||
|
std::string compute_auth(const std::string &password, const std::string &salt,
|
||||||
|
const std::string &challenge) {
|
||||||
|
const std::string secret = sha256_base64(password + salt);
|
||||||
|
return sha256_base64(secret + challenge);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string build_identify(int rpc_version, const std::string &authentication) {
|
||||||
|
rapidjson::StringBuffer sb;
|
||||||
|
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
|
||||||
|
w.StartObject();
|
||||||
|
w.Key("op"); w.Int(1);
|
||||||
|
w.Key("d");
|
||||||
|
w.StartObject();
|
||||||
|
w.Key("rpcVersion"); w.Int(rpc_version);
|
||||||
|
if (!authentication.empty()) {
|
||||||
|
w.Key("authentication"); w.String(authentication.c_str());
|
||||||
|
}
|
||||||
|
w.EndObject();
|
||||||
|
w.EndObject();
|
||||||
|
return sb.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string build_request(const std::string &request_type, uint64_t request_id) {
|
||||||
|
rapidjson::StringBuffer sb;
|
||||||
|
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
|
||||||
|
w.StartObject();
|
||||||
|
w.Key("op"); w.Int(6);
|
||||||
|
w.Key("d");
|
||||||
|
w.StartObject();
|
||||||
|
w.Key("requestType"); w.String(request_type.c_str());
|
||||||
|
w.Key("requestId"); w.String(std::to_string(request_id).c_str());
|
||||||
|
w.EndObject();
|
||||||
|
w.EndObject();
|
||||||
|
return sb.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a numeric field as int64 ms (obs sends durations as integers/doubles)
|
||||||
|
int64_t json_number(const rapidjson::Value &obj, const char *key) {
|
||||||
|
if (obj.HasMember(key) && obj[key].IsNumber()) {
|
||||||
|
return static_cast<int64_t>(obj[key].GetDouble());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool json_bool(const rapidjson::Value &obj, const char *key) {
|
||||||
|
return obj.HasMember(key) && obj[key].IsBool() && obj[key].GetBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string json_string(const rapidjson::Value &obj, const char *key) {
|
||||||
|
if (obj.HasMember(key) && obj[key].IsString()) {
|
||||||
|
return obj[key].GetString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the Identify (op 1) reply to a Hello (op 0), answering the auth
|
||||||
|
// challenge if the server requires one
|
||||||
|
std::string build_hello_response(const rapidjson::Value &d, const std::string &password) {
|
||||||
|
int rpc_version = 1;
|
||||||
|
if (d.HasMember("rpcVersion") && d["rpcVersion"].IsInt()) {
|
||||||
|
rpc_version = d["rpcVersion"].GetInt();
|
||||||
|
}
|
||||||
|
std::string auth;
|
||||||
|
if (d.HasMember("authentication") && d["authentication"].IsObject()) {
|
||||||
|
const rapidjson::Value &a = d["authentication"];
|
||||||
|
const std::string challenge = json_string(a, "challenge");
|
||||||
|
const std::string salt = json_string(a, "salt");
|
||||||
|
if (!challenge.empty()) {
|
||||||
|
auth = compute_auth(password, salt, challenge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return build_identify(rpc_version, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// map an obs-websocket outputState to a user notification. `label` is the
|
||||||
|
// output kind ("Streaming" or "Recording"). transitional states are ignored.
|
||||||
|
void notify_output_state(const char *label, const std::string &state) {
|
||||||
|
using overlay::notifications::Severity;
|
||||||
|
|
||||||
|
struct StateToast {
|
||||||
|
const char *state;
|
||||||
|
Severity severity;
|
||||||
|
const char *verb;
|
||||||
|
};
|
||||||
|
static const StateToast TOASTS[] = {
|
||||||
|
{ "OBS_WEBSOCKET_OUTPUT_STARTED", Severity::Success, "started" },
|
||||||
|
{ "OBS_WEBSOCKET_OUTPUT_STOPPED", Severity::Info, "stopped" },
|
||||||
|
{ "OBS_WEBSOCKET_OUTPUT_PAUSED", Severity::Warning, "paused" },
|
||||||
|
{ "OBS_WEBSOCKET_OUTPUT_RESUMED", Severity::Info, "resumed" },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &toast : TOASTS) {
|
||||||
|
if (state == toast.state) {
|
||||||
|
overlay::notifications::add(toast.severity,
|
||||||
|
"OBS: " + std::string(label) + " " + toast.verb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace overlay::windows {
|
||||||
|
|
||||||
|
// connection settings resolved at launch (see launcher.cpp)
|
||||||
|
bool OBS_CONTROL_ENABLED = false;
|
||||||
|
std::string OBS_CONTROL_HOST = "127.0.0.1";
|
||||||
|
uint16_t OBS_CONTROL_PORT = 4455;
|
||||||
|
std::string OBS_CONTROL_PASSWORD;
|
||||||
|
bool OBS_CONTROL_DEBUG = false;
|
||||||
|
|
||||||
|
void OBSControl::enqueue_request(const std::string &request_type) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->command_mutex);
|
||||||
|
this->command_queue.push_back(request_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OBSControl::interruptible_sleep(int total_ms) {
|
||||||
|
std::unique_lock<std::mutex> lock(this->worker_mutex);
|
||||||
|
this->worker_cv.wait_for(lock, milliseconds(total_ms),
|
||||||
|
[this] { return !this->worker_running.load(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void OBSControl::handle_message(WebSocket *ws, const std::string &message,
|
||||||
|
const std::string &password, uint64_t &request_id, bool &identified) {
|
||||||
|
|
||||||
|
rapidjson::Document doc;
|
||||||
|
if (doc.Parse(message.c_str()).HasParseError() || !doc.IsObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!doc.HasMember("op") || !doc["op"].IsInt()
|
||||||
|
|| !doc.HasMember("d") || !doc["d"].IsObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int op = doc["op"].GetInt();
|
||||||
|
const rapidjson::Value &d = doc["d"];
|
||||||
|
|
||||||
|
// send an op 6 Request; each needs a unique id (we never match replies
|
||||||
|
// back, so a simple incrementing counter is enough)
|
||||||
|
const request_fn request = [&](const char *request_type) {
|
||||||
|
ws->send(build_request(request_type, ++request_id));
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case 0: // Hello
|
||||||
|
// server greeted us: reply with Identify, solving the auth
|
||||||
|
// challenge inline if the server set a password
|
||||||
|
ws->send(build_hello_response(d, password));
|
||||||
|
break;
|
||||||
|
case 2: // Identified
|
||||||
|
this->handle_identified(identified, request);
|
||||||
|
break;
|
||||||
|
case 5: // Event
|
||||||
|
this->handle_event(d, request);
|
||||||
|
break;
|
||||||
|
case 7: // RequestResponse
|
||||||
|
this->handle_response(d);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OBSControl::handle_identified(bool &identified, const request_fn &request) {
|
||||||
|
// handshake complete: the connection is now usable for requests
|
||||||
|
identified = true;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
this->status.connected = true;
|
||||||
|
this->status.identifying = false;
|
||||||
|
this->status.connection_error.clear();
|
||||||
|
}
|
||||||
|
log_info("obs", "connected and identified");
|
||||||
|
|
||||||
|
// pull the current scene/stream/record state so the UI starts accurate
|
||||||
|
request("GetCurrentProgramScene");
|
||||||
|
request("GetStreamStatus");
|
||||||
|
request("GetRecordStatus");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OBSControl::handle_event(const rapidjson::Value &d, const request_fn &request) {
|
||||||
|
const std::string type = json_string(d, "eventType");
|
||||||
|
const bool has_data = d.HasMember("eventData") && d["eventData"].IsObject();
|
||||||
|
|
||||||
|
if (type == "StreamStateChanged") {
|
||||||
|
if (has_data) {
|
||||||
|
notify_output_state("Streaming", json_string(d["eventData"], "outputState"));
|
||||||
|
}
|
||||||
|
request("GetStreamStatus");
|
||||||
|
} else if (type == "RecordStateChanged") {
|
||||||
|
if (has_data) {
|
||||||
|
notify_output_state("Recording", json_string(d["eventData"], "outputState"));
|
||||||
|
}
|
||||||
|
request("GetRecordStatus");
|
||||||
|
} else if (type == "CurrentProgramSceneChanged" && has_data) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
this->status.current_scene = json_string(d["eventData"], "sceneName");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OBSControl::handle_response(const rapidjson::Value &d) {
|
||||||
|
const std::string type = json_string(d, "requestType");
|
||||||
|
if (type.empty() || !d.HasMember("responseData") || !d["responseData"].IsObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rapidjson::Value &rd = d["responseData"];
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
if (type == "GetCurrentProgramScene") {
|
||||||
|
// newer obs returns sceneName; older builds used the now-deprecated
|
||||||
|
// currentProgramSceneName, so prefer it then fall back
|
||||||
|
std::string scene = json_string(rd, "currentProgramSceneName");
|
||||||
|
if (scene.empty()) {
|
||||||
|
scene = json_string(rd, "sceneName");
|
||||||
|
}
|
||||||
|
this->status.current_scene = scene;
|
||||||
|
} else if (type == "GetStreamStatus") {
|
||||||
|
this->status.streaming = json_bool(rd, "outputActive");
|
||||||
|
this->status.stream_duration_ms = json_number(rd, "outputDuration");
|
||||||
|
this->status.stream_duration_base_tick = now_ms();
|
||||||
|
} else if (type == "GetRecordStatus") {
|
||||||
|
this->status.recording = json_bool(rd, "outputActive");
|
||||||
|
this->status.record_paused = json_bool(rd, "outputPaused");
|
||||||
|
this->status.record_duration_ms = json_number(rd, "outputDuration");
|
||||||
|
this->status.record_duration_base_tick = now_ms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OBSControl::run_session(WebSocket *ws, const std::string &password, uint64_t &request_id) {
|
||||||
|
|
||||||
|
// one iteration of a live connection: pump socket I/O, dispatch any
|
||||||
|
// inbound messages, flush queued user commands, then refresh status
|
||||||
|
bool identified = false;
|
||||||
|
// handle_identified() issues the first GetStreamStatus/GetRecordStatus on
|
||||||
|
// identify, so the periodic poll below just maintains the ~1s cadence
|
||||||
|
auto last_status_poll = steady_clock::now();
|
||||||
|
|
||||||
|
// send a request with the next sequential id
|
||||||
|
const auto request = [&](const char *request_type) {
|
||||||
|
ws->send(build_request(request_type, ++request_id));
|
||||||
|
};
|
||||||
|
|
||||||
|
while (this->worker_running.load() && ws->getReadyState() != WebSocket::CLOSED) {
|
||||||
|
ws->poll(100);
|
||||||
|
|
||||||
|
ws->dispatch([&](const std::string &message) {
|
||||||
|
this->handle_message(ws, message, password, request_id, identified);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ws->getReadyState() == WebSocket::CLOSED) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing may be sent until the op 2 Identified handshake completes
|
||||||
|
if (!identified) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drain user commands
|
||||||
|
std::deque<std::string> pending;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->command_mutex);
|
||||||
|
pending.swap(this->command_queue);
|
||||||
|
}
|
||||||
|
for (const auto &cmd : pending) {
|
||||||
|
ws->send(build_request(cmd, ++request_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// periodic status refresh (~1s) for live duration
|
||||||
|
const auto now = steady_clock::now();
|
||||||
|
if (now - last_status_poll >= milliseconds(1000)) {
|
||||||
|
last_status_poll = now;
|
||||||
|
request("GetStreamStatus");
|
||||||
|
request("GetRecordStatus");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return identified;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OBSControl::worker_main() {
|
||||||
|
|
||||||
|
// connection settings are resolved once at launch into globals
|
||||||
|
// (launcher.cpp, from the merged command-line + saved config options)
|
||||||
|
if (!OBS_CONTROL_ENABLED) {
|
||||||
|
log_info("obs", "disabled, not connecting");
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
this->status.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string url = "ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
|
||||||
|
const std::string password = OBS_CONTROL_PASSWORD;
|
||||||
|
|
||||||
|
// opt easywsclient's internal diagnostics in/out per the debug option
|
||||||
|
EASYWSCLIENT_LOGGING_ENABLED = OBS_CONTROL_DEBUG;
|
||||||
|
|
||||||
|
// winsock is reference-counted: the app performs its own WSAStartup at
|
||||||
|
// launch (which outlives this worker), so this paired Startup/Cleanup only
|
||||||
|
// bumps the refcount and the WSACleanup below never tears down winsock for
|
||||||
|
// the rest of the process
|
||||||
|
WSADATA wsa_data;
|
||||||
|
WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||||
|
log_info("obs", "enabled, connecting to {}", url);
|
||||||
|
|
||||||
|
uint64_t request_id = 0;
|
||||||
|
|
||||||
|
// the reconnect loop retries every 5s; latch the auth-failure warning so a
|
||||||
|
// wrong password logs once, not on every retry. reset after any identified
|
||||||
|
// session so a later genuine failure is reported again
|
||||||
|
bool auth_warning_logged = false;
|
||||||
|
|
||||||
|
// reconnect loop: keep a session alive while enabled, retrying on drop
|
||||||
|
while (this->worker_running.load()) {
|
||||||
|
|
||||||
|
// mark "connecting" for the UI before each attempt
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
this->status.disabled = false;
|
||||||
|
this->status.connected = false;
|
||||||
|
this->status.identifying = true;
|
||||||
|
this->status.connection_error.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the TCP socket and perform the WebSocket handshake; null means
|
||||||
|
// OBS is unreachable (not running / wrong port / obs-websocket off)
|
||||||
|
WebSocket::pointer ws = WebSocket::from_url(url);
|
||||||
|
if (ws == nullptr) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
this->status.identifying = false;
|
||||||
|
this->status.connection_error = "Unable to connect";
|
||||||
|
}
|
||||||
|
interruptible_sleep(5000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// blocks here pumping the connection until it closes or we stop.
|
||||||
|
// a session that never reaches "Identified" was rejected by OBS,
|
||||||
|
// overwhelmingly because the password is wrong or missing
|
||||||
|
const bool identified = this->run_session(ws, password, request_id);
|
||||||
|
|
||||||
|
// session ended: close the socket cleanly and free it
|
||||||
|
ws->close();
|
||||||
|
ws->poll();
|
||||||
|
delete ws;
|
||||||
|
|
||||||
|
if (!identified && this->worker_running.load()) {
|
||||||
|
if (!auth_warning_logged) {
|
||||||
|
log_warning("obs", "connection closed before identify; "
|
||||||
|
"OBS likely rejected authentication (check the password)");
|
||||||
|
auth_warning_logged = true;
|
||||||
|
}
|
||||||
|
} else if (identified) {
|
||||||
|
// a good session resets the latch so a future failure logs again
|
||||||
|
auth_warning_logged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// connection dropped: clear live state so the UI doesn't show stale
|
||||||
|
// scene/stream/record info while disconnected
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->status_mutex);
|
||||||
|
this->status.connected = false;
|
||||||
|
this->status.identifying = false;
|
||||||
|
if (this->status.connection_error.empty()) {
|
||||||
|
this->status.connection_error =
|
||||||
|
identified ? "Disconnected" : "Auth failed (check password)";
|
||||||
|
}
|
||||||
|
this->status.streaming = false;
|
||||||
|
this->status.recording = false;
|
||||||
|
this->status.record_paused = false;
|
||||||
|
this->status.current_scene.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear any commands queued while disconnected
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->command_mutex);
|
||||||
|
this->command_queue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait before reconnecting (interruptible)
|
||||||
|
interruptible_sleep(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
WSACleanup();
|
||||||
|
log_info("obs", "OBS overlay worker stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -876,8 +876,7 @@ namespace overlay::windows {
|
|||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
// row hover detection (invisible selectable that spans entire row)
|
ImGui::HighlightTableRowOnHover();
|
||||||
ImGui::InvisibleTableRowSelectable();
|
|
||||||
|
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user