God of War Ragnarök
Adaptive triggers never reach the pad: told it runs on a Steam Deck, the game never loads Sony's library and drops every trigger effect.
What you see
No adaptive trigger effect on a DualSense, while the game plays its rumble and, with Steam Input on, shows PlayStation buttons.
Why it happens
Steam answers that the game runs on a Steam Deck under Wine, so the game never loads Sony's library, and its scePadSetTriggerEffect wrapper drops every effect it builds.
What the fix changes
A jump at that wrapper sends each effect to the pad as the DualSense's own trigger effect, in an output report that carries only the trigger fields.
The code
The game's scePadSetTriggerEffect wrapper is found by the name it hands on, not by an address.
fixes/gowr-triggers.c · find_wrapper, lines 293–350static uint8_t *find_wrapper(char *why, size_t cap)
{
static const char name[] = "scePadSetTriggerEffect";
IMAGE_NT_HEADERS64 *nt = (IMAGE_NT_HEADERS64 *)(g_exe + ((IMAGE_DOS_HEADER *)g_exe)->e_lfanew);
IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(nt);
IMAGE_DATA_DIRECTORY *exc = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
const IMAGE_RUNTIME_FUNCTION_ENTRY *fn = (const IMAGE_RUNTIME_FUNCTION_ENTRY *)(g_exe + exc->VirtualAddress);
size_t nfn = exc->Size / sizeof(*fn), lo, hi;
uint8_t *text = NULL, *found = NULL, *s;
uint32_t text_size = 0, str_rva = 0, i, k;
int strings = 0, hits = 0;
for (k = 0; k < nt->FileHeader.NumberOfSections; k++)
{
uint8_t *b = g_exe + sec[k].VirtualAddress;
uint32_t n = sec[k].Misc.VirtualSize;
if (!memcmp(sec[k].Name, ".text", 6)) { text = b; text_size = n; continue; }
if (sec[k].Characteristics & IMAGE_SCN_MEM_EXECUTE || !(sec[k].Characteristics & IMAGE_SCN_MEM_READ)) continue;
for (s = b + 1; s + sizeof(name) <= b + n; s++)
if (*s == name[0] && !s[-1] && !memcmp(s, name, sizeof(name)))
{
strings++;
str_rva = (uint32_t)(s - g_exe);
}
}
if (strings != 1 || !text || !nfn)
{
snprintf(why, cap, "%d copies of the name, %s, %u functions", strings, text ? ".text" : "no .text", (unsigned)nfn);
return NULL;
}
for (i = 0; i + 7 <= text_size; i++)
{
uint8_t *at = text + i, *start;
uint32_t rva;
int32_t disp;
if ((at[0] != 0x48 && at[0] != 0x4c) || at[1] != 0x8d || (at[2] & 0xc7) != 0x05) continue;
memcpy(&disp, at + 3, 4);
rva = (uint32_t)(at + 7 - g_exe) + (uint32_t)disp;
if (rva != str_rva) continue;
rva = (uint32_t)(at - g_exe);
for (lo = 0, hi = nfn; lo < hi; )
{
size_t mid = (lo + hi) / 2;
if (fn[mid].BeginAddress <= rva) lo = mid + 1; else hi = mid;
}
if (!lo || rva >= fn[lo - 1].EndAddress) continue;
start = g_exe + fn[lo - 1].BeginAddress;
if (memcmp(start, wrapper_start, sizeof(wrapper_start))) continue;
hits++;
found = start;
}
if (hits != 1)
{
snprintf(why, cap, "%d functions name it and start as the wrapper does", hits);
return NULL;
}
return found;
}Sony's trigger effect modes become the DualSense's own trigger effect bytes.
fixes/gowr-triggers.c · convert, lines 114–152static void convert(uint32_t mode, const uint8_t *p, uint8_t out[11])
{
uint8_t s[10] = {0};
int i;
switch (mode)
{
case MODE_FEEDBACK: /* position 0-9, strength 1-8 */
for (i = p[0]; i < 10; i++) s[i] = p[1];
zones(s, 0x21, 0, out);
return;
case MODE_WEAPON: /* start 2-7, end start+1-8, strength 1-8 */
memset(out, 0, 11);
if (p[0] > 9 || p[1] > 9 || p[1] <= p[0] || !p[2] || p[2] > 8) { out[0] = 0x05; return; }
out[0] = 0x25;
out[1] = ((1u << p[0]) | (1u << p[1])) & 0xff;
out[2] = ((1u << p[0]) | (1u << p[1])) >> 8;
out[3] = p[2] - 1;
return;
case MODE_VIBRATION: /* position 0-9, amplitude 1-8, frequency in Hz */
for (i = p[0]; i < 10; i++) s[i] = p[1];
zones(s, 0x26, p[2], out);
return;
case MODE_MULTI_FEEDBACK: /* strength[10] */
zones(p, 0x21, 0, out);
return;
case MODE_SLOPE: /* start, end, start strength, end strength */
if (p[1] > p[0] && p[1] <= 9)
for (i = p[0]; i < 10; i++)
s[i] = i >= p[1] ? p[3] : (uint8_t)(p[2] + (int)(p[3] - p[2]) * (i - p[0]) / (p[1] - p[0]));
zones(s, 0x21, 0, out);
return;
case MODE_MULTI_VIBRATION: /* frequency, amplitude[10] */
zones(p + 1, 0x26, p[0], out);
return;
default:
memset(out, 0, 11);
out[0] = 0x05;
}
}Each effect the game builds goes to the pad, only when it changes, and the game gets the answer it always got.
fixes/gowr-triggers.c · triggers_SetTriggerEffect, lines 256–290__attribute__((noinline)) int32_t triggers_SetTriggerEffect(int32_t handle, const uint8_t *param)
{
uint8_t want[2][11];
int sides = 0, i;
(void)handle;
if (!param) return (int32_t)RETURN_NOT_LOADED;
InterlockedIncrement(&g_effects);
EnterCriticalSection(&g_lock);
if (!g_pad_looked) find_pad();
if (g_pad != INVALID_HANDLE_VALUE)
{
/* Sony's command[0] is L2 and command[1] R2; the DualSense's report has R2 first. */
for (i = 0; i < 2; i++)
{
int ds = i ? 0 : 1;
const uint8_t *cmd = param + CMD_FIRST + CMD_SIZE * i;
if (!(param[0] & (1 << i))) continue;
convert(*(const uint32_t *)cmd, cmd + CMD_PARAM, want[ds]);
if (memcmp(want[ds], g_last[ds], 11) || !g_set[ds])
{
if (!g_set[ds] || want[ds][0] != g_last[ds][0])
logline("%s: mode %u -> DualSense %02x %02x%02x %02x%02x%02x%02x .. %02x", ds ? "L2" : "R2",
*(const uint32_t *)cmd, want[ds][0], want[ds][1], want[ds][2], want[ds][3], want[ds][4],
want[ds][5], want[ds][6], want[ds][9]);
memcpy(g_last[ds], want[ds], 11);
g_set[ds] = 1;
sides |= 1 << ds;
}
}
if (sides) write_sides(want, sides);
}
LeaveCriticalSection(&g_lock);
return (int32_t)RETURN_NOT_LOADED; /* what the original answers: the game behaves as before */
}Details
- Needs
- D3DMetal, through NotProton; the fix does not depend on the backend.
- Installer
- install-gowr-triggers.sh
- Executable
- GoWR.exe
- Built from
- gowr-triggers.c