mirror of
https://github.com/gwdevhub/gMod.git
synced 2026-07-21 01:49:31 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66663fa9f9 | |||
| 34dd80388f | |||
| f420bbd50a | |||
| e762e1b716 | |||
| 13abd4e546 | |||
| 92c6b115f3 | |||
| 2b87e0e53a | |||
| b04b4583f4 | |||
| 18d650a422 | |||
| 55e36db265 | |||
| 379878bd6d | |||
| ac39241ff9 |
@@ -0,0 +1,38 @@
|
|||||||
|
# Build output and artifacts
|
||||||
|
build/
|
||||||
|
bin/
|
||||||
|
Debug/
|
||||||
|
Release/
|
||||||
|
MinSizeRel/
|
||||||
|
RelWithDebInfo/
|
||||||
|
win32/
|
||||||
|
install/
|
||||||
|
|
||||||
|
# CMake generated directories
|
||||||
|
*.dir/
|
||||||
|
CMakeFiles/
|
||||||
|
_deps/
|
||||||
|
|
||||||
|
# vcpkg vendored dependencies
|
||||||
|
vcpkg_installed/
|
||||||
|
|
||||||
|
# Visual Studio files
|
||||||
|
*.vcxproj
|
||||||
|
*.vcxproj.filters
|
||||||
|
*.vcxproj.user
|
||||||
|
*.sln
|
||||||
|
*.slnx
|
||||||
|
*.sdf
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.aps
|
||||||
|
|
||||||
|
# CMake cache/generated
|
||||||
|
CMakeCache.txt
|
||||||
|
cmake_install.cmake
|
||||||
|
ALL_BUILD*
|
||||||
|
ZERO_CHECK*
|
||||||
|
INSTALL*
|
||||||
|
|
||||||
|
# Version resource (generated)
|
||||||
|
version.rc
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Shell scripts must stay LF or bash on the CI runner chokes on \r.
|
||||||
|
*.sh text eol=lf
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Pre-configure SimplySign Desktop for unattended use:
|
||||||
|
# - auto-show the login dialog on launch (so the TOTP keystrokes have a target)
|
||||||
|
# - cache the smart-card PIN in the CSP, so signtool signs without a per-call
|
||||||
|
# PIN prompt, and forget it again when the session disconnects.
|
||||||
|
# Mirrors the settings used by the blinkdisk / docuscope CI signing setups.
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$RegistryPath = "HKCU:\Software\Certum\SimplySign"
|
||||||
|
|
||||||
|
$settings = [ordered]@{
|
||||||
|
ShowLoginDialogOnStart = 1
|
||||||
|
ShowLoginDialogOnAppRequest = 1
|
||||||
|
RememberLastUserName = 0
|
||||||
|
Autostart = 0
|
||||||
|
UnregisterCertificatesOnDisconnect = 0
|
||||||
|
RememberPINinCSP = 1
|
||||||
|
ForgetPINinCSPonDisconnect = 1
|
||||||
|
LangID = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== Configuring SimplySign Desktop registry ==="
|
||||||
|
New-Item -Path $RegistryPath -Force | Out-Null
|
||||||
|
foreach ($name in $settings.Keys) {
|
||||||
|
Set-ItemProperty -Path $RegistryPath -Name $name -Value $settings[$name] -Type DWord
|
||||||
|
Write-Host " $name = $($settings[$name])"
|
||||||
|
}
|
||||||
|
Write-Host "Done."
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
# Authenticate SimplySign Desktop non-interactively.
|
||||||
|
#
|
||||||
|
# Certum's SimplySign cloud has no headless login: the certificate only reaches
|
||||||
|
# the Windows store after the GUI client authenticates. So we generate the
|
||||||
|
# current TOTP from the otpauth:// secret (CERTUM_OTP_URI) and paste the
|
||||||
|
# credentials into the login dialog. This needs an interactive desktop session,
|
||||||
|
# which GitHub-hosted Windows runners provide.
|
||||||
|
#
|
||||||
|
# Based on https://www.devas.life/how-to-automate-signing-your-windows-app-with-certum/
|
||||||
|
# and the refinements in blinkdisk's connect-simplysign.ps1.
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$OtpUri = $env:CERTUM_OTP_URI,
|
||||||
|
[string]$UserId = $env:CERTUM_USERID,
|
||||||
|
[string]$ExePath = $env:CERTUM_EXE_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $OtpUri) { Write-Host "ERROR: CERTUM_OTP_URI not provided"; exit 1 }
|
||||||
|
if (-not $UserId) { Write-Host "ERROR: CERTUM_USERID not provided"; exit 1 }
|
||||||
|
if (-not $ExePath) {
|
||||||
|
$ExePath = "C:\Program Files\Certum\SimplySign Desktop\SimplySignDesktop.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "=== SimplySign Desktop TOTP authentication ==="
|
||||||
|
if (-not (Test-Path $ExePath)) {
|
||||||
|
Write-Host "ERROR: SimplySign Desktop not found at $ExePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Parse the otpauth:// URI (works on PowerShell 5.1 and 7+) ---------------
|
||||||
|
$uri = [Uri]$OtpUri
|
||||||
|
try {
|
||||||
|
$q = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
|
||||||
|
} catch {
|
||||||
|
$q = @{}
|
||||||
|
foreach ($part in $uri.Query.TrimStart('?') -split '&') {
|
||||||
|
$kv = $part -split '=', 2
|
||||||
|
if ($kv.Count -eq 2) { $q[$kv[0]] = [Uri]::UnescapeDataString($kv[1]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$Base32 = $q['secret']
|
||||||
|
$Digits = if ($q['digits']) { [int]$q['digits'] } else { 6 }
|
||||||
|
$Period = if ($q['period']) { [int]$q['period'] } else { 30 }
|
||||||
|
$Algorithm = if ($q['algorithm']) { $q['algorithm'].ToUpper() } else { 'SHA256' }
|
||||||
|
|
||||||
|
if (-not $Base32) { Write-Host "ERROR: otpauth URI has no 'secret'"; exit 1 }
|
||||||
|
if ($Algorithm -notin @('SHA1', 'SHA256', 'SHA512')) {
|
||||||
|
Write-Host "ERROR: unsupported TOTP algorithm: $Algorithm"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- TOTP generator (RFC 6238), inline C# so there are no dependencies --------
|
||||||
|
Add-Type -Language CSharp @"
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
public static class Totp
|
||||||
|
{
|
||||||
|
private const string B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
|
||||||
|
private static byte[] Base32Decode(string s)
|
||||||
|
{
|
||||||
|
s = s.TrimEnd('=').ToUpperInvariant();
|
||||||
|
byte[] bytes = new byte[s.Length * 5 / 8];
|
||||||
|
int bitBuffer = 0, bitsLeft = 0, idx = 0;
|
||||||
|
foreach (char c in s)
|
||||||
|
{
|
||||||
|
int val = B32.IndexOf(c);
|
||||||
|
if (val < 0) throw new ArgumentException("Invalid Base32 char: " + c);
|
||||||
|
bitBuffer = (bitBuffer << 5) | val;
|
||||||
|
bitsLeft += 5;
|
||||||
|
if (bitsLeft >= 8) { bytes[idx++] = (byte)(bitBuffer >> (bitsLeft - 8)); bitsLeft -= 8; }
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HMAC Hmac(string algorithm, byte[] key)
|
||||||
|
{
|
||||||
|
switch (algorithm.ToUpper())
|
||||||
|
{
|
||||||
|
case "SHA1": return new HMACSHA1(key);
|
||||||
|
case "SHA256": return new HMACSHA256(key);
|
||||||
|
case "SHA512": return new HMACSHA512(key);
|
||||||
|
default: throw new ArgumentException("Unsupported algorithm: " + algorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Now(string secret, int digits, int period, string algorithm)
|
||||||
|
{
|
||||||
|
byte[] key = Base32Decode(secret);
|
||||||
|
long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / period;
|
||||||
|
byte[] cnt = BitConverter.GetBytes(counter);
|
||||||
|
if (BitConverter.IsLittleEndian) Array.Reverse(cnt);
|
||||||
|
|
||||||
|
byte[] hash;
|
||||||
|
using (var h = Hmac(algorithm, key)) { hash = h.ComputeHash(cnt); }
|
||||||
|
|
||||||
|
int offset = hash[hash.Length - 1] & 0x0F;
|
||||||
|
int binary =
|
||||||
|
((hash[offset] & 0x7F) << 24) |
|
||||||
|
((hash[offset + 1] & 0xFF) << 16) |
|
||||||
|
((hash[offset + 2] & 0xFF) << 8) |
|
||||||
|
(hash[offset + 3] & 0xFF);
|
||||||
|
int otp = binary % (int)Math.Pow(10, digits);
|
||||||
|
return otp.ToString(new string('0', digits));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
function Find-UiByName($el, $walker, $name) {
|
||||||
|
$c = $walker.GetFirstChild($el)
|
||||||
|
while ($c) {
|
||||||
|
if ($c.Current.Name -eq $name) { return $c }
|
||||||
|
$r = Find-UiByName $c $walker $name
|
||||||
|
if ($r) { return $r }
|
||||||
|
$c = $walker.GetNextSibling($c)
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# An outdated SimplySign build pops a modal "New version found - download?" box
|
||||||
|
# over the login form, which swallows the credential keystrokes. Decline it by
|
||||||
|
# clicking "No" (Invoke / legacy default action / click the element's point).
|
||||||
|
function Dismiss-UpdatePrompt {
|
||||||
|
try { Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes -ErrorAction Stop } catch { return $false }
|
||||||
|
if (-not ('Win32Mouse' -as [type])) {
|
||||||
|
Add-Type @"
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
public static class Win32Mouse {
|
||||||
|
[DllImport("user32.dll")] static extern bool SetCursorPos(int x, int y);
|
||||||
|
[DllImport("user32.dll")] static extern void mouse_event(uint f, uint x, uint y, uint d, int e);
|
||||||
|
public static void Click(int x, int y){ SetCursorPos(x,y); mouse_event(0x02,0,0,0,0); mouse_event(0x04,0,0,0,0); }
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
$procIds = @((Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue).Id)
|
||||||
|
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
||||||
|
$top = $walker.GetFirstChild([System.Windows.Automation.AutomationElement]::RootElement)
|
||||||
|
while ($top) {
|
||||||
|
if ($procIds -contains $top.Current.ProcessId) {
|
||||||
|
$no = Find-UiByName $top $walker "No"
|
||||||
|
if ($no) {
|
||||||
|
Write-Host "Update prompt detected; declining (clicking 'No')."
|
||||||
|
try { ($no.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)).Invoke(); return $true } catch {}
|
||||||
|
try { ($no.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern)).DoDefaultAction(); return $true } catch {}
|
||||||
|
try { $pt = $no.GetClickablePoint(); [Win32Mouse]::Click([int]$pt.X, [int]$pt.Y); return $true } catch { Write-Host "Click 'No' failed: $($_.Exception.Message)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$top = $walker.GetNextSibling($top)
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start from a clean slate so a stale window/session can't swallow the keystrokes.
|
||||||
|
if ($existing = Get-Process -Name "SimplySignDesktop" -ErrorAction Ignore) {
|
||||||
|
Write-Host "Killing existing SimplySignDesktop process..."
|
||||||
|
$existing | Stop-Process -Force
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Launching SimplySign Desktop..."
|
||||||
|
$proc = Start-Process -FilePath $ExePath -PassThru
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
|
||||||
|
$wshell = New-Object -ComObject WScript.Shell
|
||||||
|
|
||||||
|
Write-Host "Focusing the login window..."
|
||||||
|
$focused = $wshell.AppActivate($proc.Id)
|
||||||
|
if (-not $focused) { $focused = $wshell.AppActivate('SimplySign Desktop') }
|
||||||
|
for ($i = 0; (-not $focused) -and ($i -lt 10); $i++) {
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
$focused = $wshell.AppActivate($proc.Id) -or $wshell.AppActivate('SimplySign Desktop')
|
||||||
|
}
|
||||||
|
if (-not $focused) {
|
||||||
|
Write-Host "ERROR: could not bring SimplySign Desktop to the foreground"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Decline the "new version available" modal if it's covering the login form.
|
||||||
|
if (Dismiss-UpdatePrompt) { Start-Sleep -Milliseconds 800 }
|
||||||
|
|
||||||
|
# Re-assert focus: dismissing the dialog can move the foreground window, and
|
||||||
|
# keystrokes sent to the wrong window are silently dropped.
|
||||||
|
$wshell.AppActivate($proc.Id) | Out-Null
|
||||||
|
$wshell.AppActivate('SimplySign Desktop') | Out-Null
|
||||||
|
Start-Sleep -Milliseconds 400
|
||||||
|
|
||||||
|
# Paste rather than type: SendKeys mangles characters like + ^ % ( ) and can
|
||||||
|
# drop characters; pasting delivers the exact string. Falls back to typing if
|
||||||
|
# the clipboard is unavailable.
|
||||||
|
function Set-Field([string]$text) {
|
||||||
|
try {
|
||||||
|
Set-Clipboard -Value $text -ErrorAction Stop
|
||||||
|
Start-Sleep -Milliseconds 150
|
||||||
|
$wshell.SendKeys("^v")
|
||||||
|
} catch {
|
||||||
|
Write-Host "Clipboard unavailable ($($_.Exception.Message)); typing instead."
|
||||||
|
$wshell.SendKeys($text)
|
||||||
|
}
|
||||||
|
Start-Sleep -Milliseconds 250
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Injecting credentials..."
|
||||||
|
Set-Field $UserId
|
||||||
|
$wshell.SendKeys("{TAB}")
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
|
||||||
|
# Generate the code right before sending it so it can't expire while we focus.
|
||||||
|
$otp = [Totp]::Now($Base32, $Digits, $Period, $Algorithm)
|
||||||
|
Set-Field $otp
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
$wshell.SendKeys("{ENTER}")
|
||||||
|
|
||||||
|
# Don't leave the OTP / username sitting on the clipboard afterwards.
|
||||||
|
try { Set-Clipboard -Value " " -ErrorAction Stop } catch {}
|
||||||
|
|
||||||
|
Write-Host "Waiting for authentication to settle..."
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
|
if (-not (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Host "ERROR: SimplySign Desktop exited - authentication failed."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Confirm the login actually mounted the signing certificate into the store.
|
||||||
|
$thumb = if ($env:CERTUM_CERT_SHA1) { ($env:CERTUM_CERT_SHA1 -replace '\s', '').ToUpperInvariant() } else { $null }
|
||||||
|
Write-Host "Verifying the signing certificate reached the store..."
|
||||||
|
$deadline = (Get-Date).AddSeconds(60)
|
||||||
|
$found = $false
|
||||||
|
do {
|
||||||
|
$certs = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue
|
||||||
|
if ($thumb) { $found = [bool]($certs | Where-Object { $_.Thumbprint -eq $thumb }) }
|
||||||
|
else { $found = [bool]($certs | Where-Object { $_.Subject -like '*Certum*' -or $_.Issuer -like '*Certum*' }) }
|
||||||
|
if ($found) { break }
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
} while ((Get-Date) -lt $deadline)
|
||||||
|
|
||||||
|
if ($found) {
|
||||||
|
Write-Host "=== Authentication complete: signing certificate is available. ==="
|
||||||
|
} else {
|
||||||
|
Write-Host "ERROR: signing certificate did not appear after authentication."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Install Certum SimplySign Desktop on the (ephemeral) Windows runner.
|
||||||
|
# SimplySign Desktop is what mounts the cloud signing certificate as a virtual
|
||||||
|
# smart card into the Windows certificate store; signtool then selects it by
|
||||||
|
# thumbprint. The version + checksum are pinned for reproducibility.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="9.3.4.72"
|
||||||
|
URL="https://files.certum.eu/software/SimplySignDesktop/Windows/${VERSION}/SimplySignDesktop-${VERSION}-64-bit-en.msi"
|
||||||
|
EXPECTED_SHA256="bd51ebbaaac20fc7d59ab7103b5ed532b7800586df4e31f6999d03a394f9c515"
|
||||||
|
INSTALLER="SimplySignDesktop.msi"
|
||||||
|
INSTALL_DIR="/c/Program Files/Certum/SimplySign Desktop"
|
||||||
|
|
||||||
|
echo "=== Installing Certum SimplySign Desktop ${VERSION} ==="
|
||||||
|
|
||||||
|
# Idempotent: a cached/pre-provisioned image may already have it.
|
||||||
|
if [ -d "$INSTALL_DIR" ]; then
|
||||||
|
echo "Already installed at: $INSTALL_DIR"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Downloading installer..."
|
||||||
|
curl -L "$URL" -o "$INSTALLER" --fail --max-time 600
|
||||||
|
|
||||||
|
ACTUAL_SHA256=$(sha256sum "$INSTALLER" | awk '{print $1}')
|
||||||
|
if [ "$EXPECTED_SHA256" != "$ACTUAL_SHA256" ]; then
|
||||||
|
echo "ERROR: checksum mismatch"
|
||||||
|
echo " expected: $EXPECTED_SHA256"
|
||||||
|
echo " actual: $ACTUAL_SHA256"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Checksum verified."
|
||||||
|
|
||||||
|
echo "Running msiexec (silent)..."
|
||||||
|
# Start-Process inherits this CWD, so the relative installer path resolves here.
|
||||||
|
powershell -Command "Start-Process msiexec.exe -ArgumentList '/i','${INSTALLER}','/quiet','/norestart','/l*v','install.log','ALLUSERS=1','REBOOT=ReallySuppress' -Wait -NoNewWindow"
|
||||||
|
|
||||||
|
if [ ! -d "$INSTALL_DIR" ]; then
|
||||||
|
echo "ERROR: installation directory not found after install"
|
||||||
|
echo "Last 20 lines of install.log:"
|
||||||
|
tail -20 install.log 2>/dev/null || echo "(no install.log)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "SimplySign Desktop installed."
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Sign the given files with the Certum cloud certificate (selected by SHA1
|
||||||
|
# thumbprint from the store) and verify each signature. Assumes SimplySign
|
||||||
|
# Desktop has already authenticated (connect-simplysign.ps1).
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$Files,
|
||||||
|
[string]$CertSha1 = $env:CERTUM_CERT_SHA1,
|
||||||
|
[string]$TimestampUrl = "http://time.certum.pl"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
if (-not $CertSha1) { throw "CERTUM_CERT_SHA1 not provided." }
|
||||||
|
$thumb = ($CertSha1 -replace '\s', '').ToUpperInvariant()
|
||||||
|
|
||||||
|
# Resolve the newest signtool.exe from the installed Windows SDK(s).
|
||||||
|
$kitsBin = "C:\Program Files (x86)\Windows Kits\10\bin"
|
||||||
|
$signtool = Get-ChildItem -Path $kitsBin -Recurse -Filter signtool.exe -File -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.FullName -like '*\x64\*' } |
|
||||||
|
Sort-Object { [version]$_.Directory.Parent.Name } |
|
||||||
|
Select-Object -Last 1 -ExpandProperty FullName
|
||||||
|
if (-not $signtool) { throw "signtool.exe not found under $kitsBin" }
|
||||||
|
Write-Host "Using signtool: $signtool"
|
||||||
|
|
||||||
|
# The SimplySign virtual smart card registers in the store a moment after the
|
||||||
|
# desktop client authenticates; poll for our certificate before signing.
|
||||||
|
Write-Host "Waiting for certificate $thumb to appear in the store..."
|
||||||
|
$deadline = (Get-Date).AddSeconds(120)
|
||||||
|
$cert = $null
|
||||||
|
$elapsed = 0
|
||||||
|
do {
|
||||||
|
$cert = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Thumbprint -eq $thumb } | Select-Object -First 1
|
||||||
|
if ($cert) { break }
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
$elapsed += 3
|
||||||
|
Write-Host " ...still waiting (${elapsed}s)"
|
||||||
|
} while ((Get-Date) -lt $deadline)
|
||||||
|
|
||||||
|
if (-not $cert) {
|
||||||
|
Write-Host "Certificate not found. CurrentUser\My + LocalMachine\My contents:"
|
||||||
|
Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object Thumbprint, Subject, HasPrivateKey | Format-Table -AutoSize | Out-String | Write-Host
|
||||||
|
Write-Host "SimplySign processes:"
|
||||||
|
Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object Name, Id, Responding, MainWindowTitle | Format-Table -AutoSize | Out-String | Write-Host
|
||||||
|
throw "Certificate $thumb not found in the store - SimplySign authentication likely failed (see dump above)."
|
||||||
|
}
|
||||||
|
Write-Host "Found signing certificate: $($cert.Subject)"
|
||||||
|
|
||||||
|
$failed = @()
|
||||||
|
foreach ($file in $Files) {
|
||||||
|
if (-not (Test-Path $file)) { throw "File to sign not found: $file" }
|
||||||
|
Write-Host "=== Signing $file ==="
|
||||||
|
& $signtool sign /sha1 $thumb /fd sha256 /td sha256 /tr $TimestampUrl /v $file
|
||||||
|
if ($LASTEXITCODE -ne 0) { $failed += $file; continue }
|
||||||
|
& $signtool verify /pa /v $file
|
||||||
|
if ($LASTEXITCODE -ne 0) { $failed += $file }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($failed.Count -gt 0) {
|
||||||
|
throw "Signing/verification failed for: $($failed -join ', ')"
|
||||||
|
}
|
||||||
|
Write-Host "All binaries signed and verified."
|
||||||
@@ -16,9 +16,15 @@ jobs:
|
|||||||
|
|
||||||
runs-on: windows-2025-vs2026
|
runs-on: windows-2025-vs2026
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
Configuration: Release
|
Configuration: Release
|
||||||
Actions_Allow_Unsecure_Commands: true
|
Actions_Allow_Unsecure_Commands: true
|
||||||
|
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
|
||||||
|
CERTUM_USERID: ${{ secrets.CERTUM_USERID }}
|
||||||
|
CERTUM_CERT_SHA1: ${{ secrets.CERTUM_CERT_SHA1 }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -46,7 +52,28 @@ jobs:
|
|||||||
|
|
||||||
- name: Build binaries
|
- name: Build binaries
|
||||||
run: cmake --build build --config Release
|
run: cmake --build build --config Release
|
||||||
|
|
||||||
|
# Signing only runs when the Certum secrets are present (i.e. on the real
|
||||||
|
# repo, not forks); without them the build still produces unsigned binaries.
|
||||||
|
- name: Set up Certum SimplySign
|
||||||
|
if: env.CERTUM_OTP_URI != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
chmod +x ./.github/scripts/install-simplysign.sh
|
||||||
|
./.github/scripts/install-simplysign.sh
|
||||||
|
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign.ps1"
|
||||||
|
|
||||||
|
- name: Authenticate Certum SimplySign
|
||||||
|
if: env.CERTUM_OTP_URI != ''
|
||||||
|
shell: bash
|
||||||
|
run: powershell -ExecutionPolicy Bypass -File "./.github/scripts/connect-simplysign.ps1"
|
||||||
|
|
||||||
|
- name: Sign release binaries
|
||||||
|
if: env.CERTUM_OTP_URI != ''
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
./.github/scripts/sign-certum.ps1 -Files @(".\bin\Release\gMod.dll", ".\bin\Release\TpfConvert.exe")
|
||||||
|
|
||||||
- name: Retrieve version
|
- name: Retrieve version
|
||||||
id: set_version
|
id: set_version
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# gMod
|
||||||
|
|
||||||
|
A 32-bit Windows DLL that hooks Direct3D 9 to load texture mods for Guild Wars. Continuation of uMod. Integrated with [GW Launcher](https://github.com/gwdevhub/gwlauncher) and [Daybreak](https://github.com/gwdevhub/Daybreak).
|
||||||
|
|
||||||
|
## Project structure
|
||||||
|
|
||||||
|
- `header/` — header files (D3D9Hooks, D3D9State, Defines, Error, Main, Utils)
|
||||||
|
- `source/` — implementation files (D3D9Hooks.cpp, dll_main.cpp, Error.cpp)
|
||||||
|
- `modules/` — C++23 module files (ModfileLoader, ModfileLoader_TpfReader, TextureClient, TextureFunction)
|
||||||
|
- `TpfConvert/` — standalone utility to fix broken .tpf files
|
||||||
|
- `cmake/` — CMake helper scripts
|
||||||
|
- `build/` — CMake build tree (generated, ignored)
|
||||||
|
- `bin/` — compiled output (generated, ignored)
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Requires Visual Studio 2022, CMake 3.29+, and vcpkg — all via the VS 2022 Developer PowerShell.
|
||||||
|
|
||||||
|
```
|
||||||
|
cmake --preset=vcpkg
|
||||||
|
cmake --open build
|
||||||
|
# then compile inside Visual Studio, or:
|
||||||
|
cmake --build build --config Release
|
||||||
|
```
|
||||||
|
|
||||||
|
- Target: 32-bit only (`-A Win32` / `CMAKE_GENERATOR_PLATFORM=win32`)
|
||||||
|
- Standard: C++23
|
||||||
|
- Output: `bin/` (gMod.dll, TpfConvert.exe)
|
||||||
|
- Version: set in `CMakeLists.txt` (`VERSION_MAJOR/MINOR/PATCH/TWEAK`)
|
||||||
|
|
||||||
|
> **Cannot be built or run in this agent environment** (Linux, no MSVC, no 32-bit
|
||||||
|
> D3D9, no Guild Wars to inject into). Don't burn turns attempting `cmake`/builds
|
||||||
|
> here — validate changes by reading and matching existing patterns, and lean on
|
||||||
|
> Windows CI (`ci.yaml`) for the real build. When an actual compile/test is needed,
|
||||||
|
> ask the user to do it in Visual Studio rather than trying locally.
|
||||||
|
|
||||||
|
## Key dependencies (via vcpkg)
|
||||||
|
|
||||||
|
- `minhook` — D3D9 function hooking
|
||||||
|
- `libzippp` / `libzip` / `zlib` — reading .tpf/.zip mod files
|
||||||
|
- `directxtex` — texture format conversion
|
||||||
|
- `Microsoft.GSL` — Guidelines Support Library
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. `dll_main.cpp` — DLL entry point, hooks d3d9.dll via MinHook
|
||||||
|
2. `D3D9Hooks.cpp` — intercepts `Direct3DCreate9` and IDirect3DDevice9 methods
|
||||||
|
3. `ModfileLoader` — reads `modlist.txt`, loads .tpf/.zip mod archives
|
||||||
|
4. `TextureClient` / `TextureFunction` — matches and replaces textures by hash at runtime
|
||||||
|
|
||||||
|
### Hooking model (read before touching D3D9 code)
|
||||||
|
|
||||||
|
gMod uses **MinHook vtable hooks**, not proxy/subclass wrappers (the old uMod model
|
||||||
|
was replaced — see `git log` "Replace D3D9 proxy wrappers with vtable hooks"). The
|
||||||
|
game keeps the **real** `IDirect3D9` / `IDirect3DDevice9` / texture objects byte-for-byte
|
||||||
|
untouched; gMod patches only the individual vtable slots it cares about (CreateDevice,
|
||||||
|
CreateTexture/Volume/Cube, UpdateTexture, BeginScene, Set/GetTexture, Release).
|
||||||
|
|
||||||
|
- Per-texture side-state lives **out of band** in `header/D3D9State.h` (`TexState`,
|
||||||
|
keyed by the real texture pointer), so reverting the hooks fully detaches gMod.
|
||||||
|
- `RemoveAllD3D9Hooks()` must leave the host process pristine — preserve that invariant.
|
||||||
|
- A vtable is shared by all instances of a class, so each slot is hooked once; texture
|
||||||
|
Release slots are hooked lazily on first instance. Don't double-hook.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Formatting is CI-enforced** by clang-format `22.1.5` (`.clang-format`, applied by
|
||||||
|
`lint.yaml` on push to `dev`). Don't hand-reformat or "tidy" whole files — it just
|
||||||
|
churns the diff. Notable rules: `ColumnLimit: 0` (never reflow long D3D signatures /
|
||||||
|
vtable typedefs), `SortIncludes: Never` (include order is load-bearing in Windows
|
||||||
|
headers — never reorder), 4-space indent, braces on their own line after functions.
|
||||||
|
- **C++23 named modules** (`modules/*.ixx`). Editing a module *interface* triggers wide
|
||||||
|
rebuilds; prefer changing implementation over interface when possible.
|
||||||
|
- **Public exported API is a fixed surface** — four `extern "C" __declspec(dllexport)`
|
||||||
|
functions in `dll_main.cpp`: `SetDevice`, `AddFile`, `RemoveFile`, `GetFiles`. These
|
||||||
|
are consumed by GW Launcher and Daybreak; do **not** change their names, signatures,
|
||||||
|
or calling convention (`__cdecl`) without coordinating downstream.
|
||||||
|
- **Commits**: short imperative summaries, lowercase, no trailing period
|
||||||
|
(e.g. "Keep loaded_files in load order so GetFiles returns priority order").
|
||||||
|
- **Branching**: `dev` is the integration branch; PRs target `master`. CI builds on
|
||||||
|
push to `dev` and on PRs to `master`.
|
||||||
|
|
||||||
|
### Where things live (by size / how often they change)
|
||||||
|
|
||||||
|
- `modules/TextureClient.ixx` (~670 lines) — texture match/replace bookkeeping, hot
|
||||||
|
- `source/D3D9Hooks.cpp` (~580) — the vtable detours and install/remove
|
||||||
|
- `modules/TextureFunction.ixx` (~380) — texture creation/conversion helpers
|
||||||
|
- `source/dll_main.cpp` (~345) — entry point + exported C API
|
||||||
|
- `modules/ModfileLoader*.ixx` — .tpf/.zip parsing
|
||||||
|
- `header/Defines.h` — shared structs/constants (`TextureFileStruct`, `HashTuple`, etc.)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Must be injected before d3d9.dll loads, or renamed to d3d9.dll in the GW folder
|
||||||
|
- Reshade > 5.0.1 causes glitches; recommend 5.0.1 or 4.9.1
|
||||||
|
- `modlist.txt` contains full paths to .tpf/.zip mod files, one per line
|
||||||
+1
-1
@@ -10,7 +10,7 @@ if(NOT(CMAKE_SIZEOF_VOID_P EQUAL 4))
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(VERSION_MAJOR 1)
|
set(VERSION_MAJOR 1)
|
||||||
set(VERSION_MINOR 9)
|
set(VERSION_MINOR 10)
|
||||||
set(VERSION_PATCH 0)
|
set(VERSION_PATCH 0)
|
||||||
set(VERSION_TWEAK 0)
|
set(VERSION_TWEAK 0)
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,8 @@ void InstallD3D9Hooks(IDirect3D9* d3d9, bool is_ex);
|
|||||||
|
|
||||||
void RemoveAllD3D9Hooks();
|
void RemoveAllD3D9Hooks();
|
||||||
|
|
||||||
|
// Delete every per-device TextureClient, releasing its replacement textures. Only safe
|
||||||
|
// on a real FreeLibrary (device still alive), not during process termination.
|
||||||
|
void DestroyAllTextureClients();
|
||||||
|
|
||||||
bool RegisterExistingDevice(IDirect3DDevice9* device);
|
bool RegisterExistingDevice(IDirect3DDevice9* device);
|
||||||
|
|||||||
@@ -159,10 +159,15 @@ TextureClient::~TextureClient()
|
|||||||
std::lock_guard lk(registry_mutex);
|
std::lock_guard lk(registry_mutex);
|
||||||
shutting_down = true; // the texture Release hook now no-ops for our textures
|
shutting_down = true; // the texture Release hook now no-ops for our textures
|
||||||
|
|
||||||
// Drop side-state but don't Release the textures: the game already released
|
// Release replacements we still own (still partnered) so they aren't leaked when
|
||||||
// its originals (and our fakes with them), and the device may be gone.
|
// torn down with the device alive (FreeLibrary). Orphaned fakes were already
|
||||||
for (const auto state : fakes | std::views::values) {
|
// released; a device-release teardown leaves the maps empty, so this is a no-op.
|
||||||
delete state;
|
for (const auto fake : fakes | std::views::values) {
|
||||||
|
if (fake->partner != nullptr) {
|
||||||
|
fake->partner->partner = nullptr; // detach the original's back-pointer
|
||||||
|
if (fake->real) fake->real->Release();
|
||||||
|
}
|
||||||
|
delete fake;
|
||||||
}
|
}
|
||||||
fakes.clear();
|
fakes.clear();
|
||||||
for (const auto state : originals | std::views::values) {
|
for (const auto state : originals | std::views::values) {
|
||||||
|
|||||||
+71
-20
@@ -2,6 +2,7 @@
|
|||||||
#include "D3D9Hooks.h"
|
#include "D3D9Hooks.h"
|
||||||
#include "MinHook.h"
|
#include "MinHook.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -31,6 +32,10 @@ namespace {
|
|||||||
// --- IDirect3D*Texture9 (IUnknown layout) ---------------------------
|
// --- IDirect3D*Texture9 (IUnknown layout) ---------------------------
|
||||||
constexpr int kResource_Release = 2;
|
constexpr int kResource_Release = 2;
|
||||||
|
|
||||||
|
// Bytes to flush from a hook target / trampoline so the x86-on-ARM64 emulator re-JITs
|
||||||
|
// it. Covers MinHook's max prologue patch and its 64-byte trampoline slot.
|
||||||
|
constexpr SIZE_T kFlushSpan = 64;
|
||||||
|
|
||||||
void** GetVTable(void* com_object)
|
void** GetVTable(void* com_object)
|
||||||
{
|
{
|
||||||
return *static_cast<void***>(com_object);
|
return *static_cast<void***>(com_object);
|
||||||
@@ -72,6 +77,9 @@ namespace {
|
|||||||
|
|
||||||
std::vector<void*> g_hooked_targets;
|
std::vector<void*> g_hooked_targets;
|
||||||
|
|
||||||
|
// Set when teardown begins; a detour still reached after this just calls the original.
|
||||||
|
std::atomic<bool> g_unhooked{false};
|
||||||
|
|
||||||
// device -> owning TextureClient
|
// device -> owning TextureClient
|
||||||
std::mutex g_devices_mutex;
|
std::mutex g_devices_mutex;
|
||||||
std::unordered_map<IDirect3DDevice9*, TextureClient*> g_devices;
|
std::unordered_map<IDirect3DDevice9*, TextureClient*> g_devices;
|
||||||
@@ -94,6 +102,13 @@ namespace {
|
|||||||
Warning("D3D9Hooks: MH_EnableHook failed for %p\n", target);
|
Warning("D3D9Hooks: MH_EnableHook failed for %p\n", target);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Under the x86-on-ARM64 emulator, patched code is only re-translated when its
|
||||||
|
// instruction cache is flushed. MinHook flushes just the 5 patched bytes, which can
|
||||||
|
// leave a stale/guarded JIT block and trap; flush the whole prologue and the freshly
|
||||||
|
// built trampoline so the emulator re-JITs both. No-op on native x86.
|
||||||
|
const HANDLE proc = GetCurrentProcess();
|
||||||
|
FlushInstructionCache(proc, target, kFlushSpan);
|
||||||
|
if (original && *original) FlushInstructionCache(proc, *original, kFlushSpan);
|
||||||
g_hooked_targets.push_back(target);
|
g_hooked_targets.push_back(target);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -174,6 +189,7 @@ namespace {
|
|||||||
D3DPRESENT_PARAMETERS* pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface)
|
D3DPRESENT_PARAMETERS* pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface)
|
||||||
{
|
{
|
||||||
const HRESULT hr = o_CreateDevice(self, Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, ppReturnedDeviceInterface);
|
const HRESULT hr = o_CreateDevice(self, Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, ppReturnedDeviceInterface);
|
||||||
|
if (g_unhooked) return hr;
|
||||||
if (SUCCEEDED(hr) && ppReturnedDeviceInterface && *ppReturnedDeviceInterface) {
|
if (SUCCEEDED(hr) && ppReturnedDeviceInterface && *ppReturnedDeviceInterface) {
|
||||||
OnDeviceCreated(*ppReturnedDeviceInterface);
|
OnDeviceCreated(*ppReturnedDeviceInterface);
|
||||||
}
|
}
|
||||||
@@ -184,6 +200,7 @@ namespace {
|
|||||||
D3DPRESENT_PARAMETERS* pPresentationParameters, D3DDISPLAYMODEEX* pFullscreenDisplayMode, IDirect3DDevice9Ex** ppReturnedDeviceInterface)
|
D3DPRESENT_PARAMETERS* pPresentationParameters, D3DDISPLAYMODEEX* pFullscreenDisplayMode, IDirect3DDevice9Ex** ppReturnedDeviceInterface)
|
||||||
{
|
{
|
||||||
const HRESULT hr = o_CreateDeviceEx(self, Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, pFullscreenDisplayMode, ppReturnedDeviceInterface);
|
const HRESULT hr = o_CreateDeviceEx(self, Adapter, DeviceType, hFocusWindow, BehaviorFlags, pPresentationParameters, pFullscreenDisplayMode, ppReturnedDeviceInterface);
|
||||||
|
if (g_unhooked) return hr;
|
||||||
if (SUCCEEDED(hr) && ppReturnedDeviceInterface && *ppReturnedDeviceInterface) {
|
if (SUCCEEDED(hr) && ppReturnedDeviceInterface && *ppReturnedDeviceInterface) {
|
||||||
OnDeviceCreated(*ppReturnedDeviceInterface);
|
OnDeviceCreated(*ppReturnedDeviceInterface);
|
||||||
}
|
}
|
||||||
@@ -193,6 +210,7 @@ namespace {
|
|||||||
ULONG STDMETHODCALLTYPE h_DeviceRelease(IDirect3DDevice9* self)
|
ULONG STDMETHODCALLTYPE h_DeviceRelease(IDirect3DDevice9* self)
|
||||||
{
|
{
|
||||||
const ULONG count = o_DeviceRelease(self);
|
const ULONG count = o_DeviceRelease(self);
|
||||||
|
if (g_unhooked) return count;
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
TextureClient* client = nullptr;
|
TextureClient* client = nullptr;
|
||||||
{
|
{
|
||||||
@@ -212,6 +230,7 @@ namespace {
|
|||||||
IDirect3DTexture9** ppTexture, HANDLE* pSharedHandle)
|
IDirect3DTexture9** ppTexture, HANDLE* pSharedHandle)
|
||||||
{
|
{
|
||||||
const HRESULT hr = o_CreateTexture(self, Width, Height, Levels, Usage, Format, Pool, ppTexture, pSharedHandle);
|
const HRESULT hr = o_CreateTexture(self, Width, Height, Levels, Usage, Format, Pool, ppTexture, pSharedHandle);
|
||||||
|
if (g_unhooked) return hr;
|
||||||
if (SUCCEEDED(hr) && ppTexture && *ppTexture) {
|
if (SUCCEEDED(hr) && ppTexture && *ppTexture) {
|
||||||
InstallTextureReleaseHook(*ppTexture, TexType::Tex2D);
|
InstallTextureReleaseHook(*ppTexture, TexType::Tex2D);
|
||||||
if (auto* client = ClientFor(self))
|
if (auto* client = ClientFor(self))
|
||||||
@@ -224,6 +243,7 @@ namespace {
|
|||||||
IDirect3DVolumeTexture9** ppVolumeTexture, HANDLE* pSharedHandle)
|
IDirect3DVolumeTexture9** ppVolumeTexture, HANDLE* pSharedHandle)
|
||||||
{
|
{
|
||||||
const HRESULT hr = o_CreateVolumeTexture(self, Width, Height, Depth, Levels, Usage, Format, Pool, ppVolumeTexture, pSharedHandle);
|
const HRESULT hr = o_CreateVolumeTexture(self, Width, Height, Depth, Levels, Usage, Format, Pool, ppVolumeTexture, pSharedHandle);
|
||||||
|
if (g_unhooked) return hr;
|
||||||
if (SUCCEEDED(hr) && ppVolumeTexture && *ppVolumeTexture) {
|
if (SUCCEEDED(hr) && ppVolumeTexture && *ppVolumeTexture) {
|
||||||
InstallTextureReleaseHook(*ppVolumeTexture, TexType::Volume);
|
InstallTextureReleaseHook(*ppVolumeTexture, TexType::Volume);
|
||||||
if (auto* client = ClientFor(self))
|
if (auto* client = ClientFor(self))
|
||||||
@@ -236,6 +256,7 @@ namespace {
|
|||||||
IDirect3DCubeTexture9** ppCubeTexture, HANDLE* pSharedHandle)
|
IDirect3DCubeTexture9** ppCubeTexture, HANDLE* pSharedHandle)
|
||||||
{
|
{
|
||||||
const HRESULT hr = o_CreateCubeTexture(self, EdgeLength, Levels, Usage, Format, Pool, ppCubeTexture, pSharedHandle);
|
const HRESULT hr = o_CreateCubeTexture(self, EdgeLength, Levels, Usage, Format, Pool, ppCubeTexture, pSharedHandle);
|
||||||
|
if (g_unhooked) return hr;
|
||||||
if (SUCCEEDED(hr) && ppCubeTexture && *ppCubeTexture) {
|
if (SUCCEEDED(hr) && ppCubeTexture && *ppCubeTexture) {
|
||||||
InstallTextureReleaseHook(*ppCubeTexture, TexType::Cube);
|
InstallTextureReleaseHook(*ppCubeTexture, TexType::Cube);
|
||||||
if (auto* client = ClientFor(self))
|
if (auto* client = ClientFor(self))
|
||||||
@@ -248,6 +269,7 @@ namespace {
|
|||||||
{
|
{
|
||||||
// Pass the real textures through, then re-evaluate mods on the changed destination.
|
// Pass the real textures through, then re-evaluate mods on the changed destination.
|
||||||
const HRESULT hr = o_UpdateTexture(self, pSourceTexture, pDestinationTexture);
|
const HRESULT hr = o_UpdateTexture(self, pSourceTexture, pDestinationTexture);
|
||||||
|
if (g_unhooked) return hr;
|
||||||
if (SUCCEEDED(hr)) {
|
if (SUCCEEDED(hr)) {
|
||||||
if (auto* client = ClientFor(self))
|
if (auto* client = ClientFor(self))
|
||||||
client->OnUpdateTexture(pSourceTexture, pDestinationTexture);
|
client->OnUpdateTexture(pSourceTexture, pDestinationTexture);
|
||||||
@@ -257,6 +279,7 @@ namespace {
|
|||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE h_BeginScene(IDirect3DDevice9* self)
|
HRESULT STDMETHODCALLTYPE h_BeginScene(IDirect3DDevice9* self)
|
||||||
{
|
{
|
||||||
|
if (g_unhooked) return o_BeginScene(self);
|
||||||
if (auto* client = ClientFor(self))
|
if (auto* client = ClientFor(self))
|
||||||
client->OnBeginScene();
|
client->OnBeginScene();
|
||||||
return o_BeginScene(self);
|
return o_BeginScene(self);
|
||||||
@@ -264,6 +287,7 @@ namespace {
|
|||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE h_SetTexture(IDirect3DDevice9* self, DWORD Stage, IDirect3DBaseTexture9* pTexture)
|
HRESULT STDMETHODCALLTYPE h_SetTexture(IDirect3DDevice9* self, DWORD Stage, IDirect3DBaseTexture9* pTexture)
|
||||||
{
|
{
|
||||||
|
if (g_unhooked) return o_SetTexture(self, Stage, pTexture);
|
||||||
IDirect3DBaseTexture9* bind = pTexture;
|
IDirect3DBaseTexture9* bind = pTexture;
|
||||||
if (auto* client = ClientFor(self))
|
if (auto* client = ClientFor(self))
|
||||||
bind = client->ResolveBinding(pTexture); // substitute the fake when modded
|
bind = client->ResolveBinding(pTexture); // substitute the fake when modded
|
||||||
@@ -273,6 +297,7 @@ namespace {
|
|||||||
HRESULT STDMETHODCALLTYPE h_GetTexture(IDirect3DDevice9* self, DWORD Stage, IDirect3DBaseTexture9** ppTexture)
|
HRESULT STDMETHODCALLTYPE h_GetTexture(IDirect3DDevice9* self, DWORD Stage, IDirect3DBaseTexture9** ppTexture)
|
||||||
{
|
{
|
||||||
const HRESULT hr = o_GetTexture(self, Stage, ppTexture);
|
const HRESULT hr = o_GetTexture(self, Stage, ppTexture);
|
||||||
|
if (g_unhooked) return hr;
|
||||||
if (SUCCEEDED(hr) && ppTexture && *ppTexture) {
|
if (SUCCEEDED(hr) && ppTexture && *ppTexture) {
|
||||||
if (auto* client = ClientFor(self)) {
|
if (auto* client = ClientFor(self)) {
|
||||||
if (auto* original = client->ResolveOriginalFromFake(*ppTexture)) {
|
if (auto* original = client->ResolveOriginalFromFake(*ppTexture)) {
|
||||||
@@ -295,6 +320,7 @@ namespace {
|
|||||||
ULONG STDMETHODCALLTYPE h_Tex2DRelease(IUnknown* self)
|
ULONG STDMETHODCALLTYPE h_Tex2DRelease(IUnknown* self)
|
||||||
{
|
{
|
||||||
const ULONG count = o_Tex2DRelease(self);
|
const ULONG count = o_Tex2DRelease(self);
|
||||||
|
if (g_unhooked) return count;
|
||||||
ReleaseTextureCleanup(self, count);
|
ReleaseTextureCleanup(self, count);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -302,6 +328,7 @@ namespace {
|
|||||||
ULONG STDMETHODCALLTYPE h_VolumeRelease(IUnknown* self)
|
ULONG STDMETHODCALLTYPE h_VolumeRelease(IUnknown* self)
|
||||||
{
|
{
|
||||||
const ULONG count = o_VolumeRelease(self);
|
const ULONG count = o_VolumeRelease(self);
|
||||||
|
if (g_unhooked) return count;
|
||||||
ReleaseTextureCleanup(self, count);
|
ReleaseTextureCleanup(self, count);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -309,6 +336,7 @@ namespace {
|
|||||||
ULONG STDMETHODCALLTYPE h_CubeRelease(IUnknown* self)
|
ULONG STDMETHODCALLTYPE h_CubeRelease(IUnknown* self)
|
||||||
{
|
{
|
||||||
const ULONG count = o_CubeRelease(self);
|
const ULONG count = o_CubeRelease(self);
|
||||||
|
if (g_unhooked) return count;
|
||||||
ReleaseTextureCleanup(self, count);
|
ReleaseTextureCleanup(self, count);
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -317,6 +345,7 @@ namespace {
|
|||||||
void InstallD3D9Hooks(IDirect3D9* d3d9, const bool is_ex)
|
void InstallD3D9Hooks(IDirect3D9* d3d9, const bool is_ex)
|
||||||
{
|
{
|
||||||
if (d3d9 == nullptr || g_d3d9_hooks_installed) return;
|
if (d3d9 == nullptr || g_d3d9_hooks_installed) return;
|
||||||
|
g_unhooked = false; // re-arm in case a prior teardown left the flag set
|
||||||
void** vt = GetVTable(d3d9);
|
void** vt = GetVTable(d3d9);
|
||||||
Hook(vt[kIDirect3D9_CreateDevice], &h_CreateDevice, reinterpret_cast<void**>(&o_CreateDevice));
|
Hook(vt[kIDirect3D9_CreateDevice], &h_CreateDevice, reinterpret_cast<void**>(&o_CreateDevice));
|
||||||
if (is_ex) {
|
if (is_ex) {
|
||||||
@@ -333,15 +362,21 @@ bool RegisterExistingDevice(IDirect3DDevice9* device)
|
|||||||
if (g_devices.contains(device)) return false; // already known
|
if (g_devices.contains(device)) return false; // already known
|
||||||
if (!g_devices.empty()) return false; // gMod supports a single device at a time
|
if (!g_devices.empty()) return false; // gMod supports a single device at a time
|
||||||
}
|
}
|
||||||
|
g_unhooked = false; // re-arm in case a prior teardown left the flag set
|
||||||
OnDeviceCreated(device); // installs device hooks + TextureClient (idempotent)
|
OnDeviceCreated(device); // installs device hooks + TextureClient (idempotent)
|
||||||
return ClientFor(device) != nullptr;
|
return ClientFor(device) != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemoveAllD3D9Hooks()
|
void RemoveAllD3D9Hooks()
|
||||||
{
|
{
|
||||||
|
// Signal first: a detour reached after this (e.g. a surviving patch) falls through.
|
||||||
|
g_unhooked = true;
|
||||||
|
|
||||||
|
const HANDLE proc = GetCurrentProcess();
|
||||||
for (void* target : g_hooked_targets) {
|
for (void* target : g_hooked_targets) {
|
||||||
MH_DisableHook(target);
|
MH_DisableHook(target);
|
||||||
MH_RemoveHook(target);
|
MH_RemoveHook(target);
|
||||||
|
FlushInstructionCache(proc, target, kFlushSpan); // re-JIT the restored prologue under emulation
|
||||||
}
|
}
|
||||||
g_hooked_targets.clear();
|
g_hooked_targets.clear();
|
||||||
|
|
||||||
@@ -351,19 +386,23 @@ void RemoveAllD3D9Hooks()
|
|||||||
g_volume_release_installed = false;
|
g_volume_release_installed = false;
|
||||||
g_cube_release_installed = false;
|
g_cube_release_installed = false;
|
||||||
|
|
||||||
o_CreateDevice = nullptr;
|
// Leave the o_* trampoline pointers intact so a surviving detour can still call through.
|
||||||
o_CreateDeviceEx = nullptr;
|
}
|
||||||
o_DeviceRelease = nullptr;
|
|
||||||
o_CreateTexture = nullptr;
|
void DestroyAllTextureClients()
|
||||||
o_CreateVolumeTexture = nullptr;
|
{
|
||||||
o_CreateCubeTexture = nullptr;
|
// Collect under the lock, delete outside it (~TextureClient takes its own locks).
|
||||||
o_UpdateTexture = nullptr;
|
std::vector<TextureClient*> clients;
|
||||||
o_BeginScene = nullptr;
|
{
|
||||||
o_SetTexture = nullptr;
|
std::lock_guard lk(g_devices_mutex);
|
||||||
o_GetTexture = nullptr;
|
for (const auto& entry : g_devices) {
|
||||||
o_Tex2DRelease = nullptr;
|
clients.push_back(entry.second);
|
||||||
o_VolumeRelease = nullptr;
|
}
|
||||||
o_CubeRelease = nullptr;
|
g_devices.clear();
|
||||||
|
}
|
||||||
|
for (auto* client : clients) {
|
||||||
|
delete client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All three read state->real directly; gMod never swaps the underlying resource.
|
// All three read state->real directly; gMod never swaps the underlying resource.
|
||||||
@@ -400,6 +439,11 @@ namespace {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The surface GetRenderTargetData reads from: either the level itself, or a
|
||||||
|
// non-multisampled resolve of it. Non-owning; pSurfaceLevel_orig and
|
||||||
|
// pResolvedSurface each own a ref and are released exactly once below.
|
||||||
|
IDirect3DSurface9* pCopySource = pSurfaceLevel_orig;
|
||||||
|
|
||||||
if (desc.MultiSampleType != D3DMULTISAMPLE_NONE) {
|
if (desc.MultiSampleType != D3DMULTISAMPLE_NONE) {
|
||||||
if (D3D_OK != device->CreateRenderTarget(desc.Width, desc.Height, desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &pResolvedSurface, nullptr)) {
|
if (D3D_OK != device->CreateRenderTarget(desc.Width, desc.Height, desc.Format, D3DMULTISAMPLE_NONE, 0, FALSE, &pResolvedSurface, nullptr)) {
|
||||||
pSurfaceLevel_orig->Release();
|
pSurfaceLevel_orig->Release();
|
||||||
@@ -407,31 +451,39 @@ namespace {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (D3D_OK != device->StretchRect(pSurfaceLevel_orig, nullptr, pResolvedSurface, nullptr, D3DTEXF_NONE)) {
|
if (D3D_OK != device->StretchRect(pSurfaceLevel_orig, nullptr, pResolvedSurface, nullptr, D3DTEXF_NONE)) {
|
||||||
|
pResolvedSurface->Release();
|
||||||
|
pResolvedSurface = nullptr;
|
||||||
pSurfaceLevel_orig->Release();
|
pSurfaceLevel_orig->Release();
|
||||||
Warning("GetTextureHash(2D) Failed: StretchRect (D3DPOOL_DEFAULT)\n");
|
Warning("GetTextureHash(2D) Failed: StretchRect (D3DPOOL_DEFAULT)\n");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
pSurfaceLevel_orig = pResolvedSurface;
|
pCopySource = pResolvedSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (D3D_OK != device->CreateOffscreenPlainSurface(desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &pOffscreenSurface, nullptr)) {
|
if (D3D_OK != device->CreateOffscreenPlainSurface(desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &pOffscreenSurface, nullptr)) {
|
||||||
pSurfaceLevel_orig->Release();
|
|
||||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
||||||
|
pSurfaceLevel_orig->Release();
|
||||||
Warning("GetTextureHash(2D) Failed: CreateOffscreenPlainSurface (D3DPOOL_DEFAULT)\n");
|
Warning("GetTextureHash(2D) Failed: CreateOffscreenPlainSurface (D3DPOOL_DEFAULT)\n");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (D3D_OK != device->GetRenderTargetData(pSurfaceLevel_orig, pOffscreenSurface)) {
|
if (D3D_OK != device->GetRenderTargetData(pCopySource, pOffscreenSurface)) {
|
||||||
pSurfaceLevel_orig->Release();
|
|
||||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
|
||||||
pOffscreenSurface->Release();
|
pOffscreenSurface->Release();
|
||||||
|
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
||||||
|
pSurfaceLevel_orig->Release();
|
||||||
Warning("GetTextureHash(2D) Failed: GetRenderTargetData (D3DPOOL_DEFAULT)\n");
|
Warning("GetTextureHash(2D) Failed: GetRenderTargetData (D3DPOOL_DEFAULT)\n");
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy is done; both source surfaces are finished with. Null pResolvedSurface
|
||||||
|
// so the shared cleanup below treats this as the pOffscreenSurface case.
|
||||||
|
if (pResolvedSurface != nullptr) {
|
||||||
|
pResolvedSurface->Release();
|
||||||
|
pResolvedSurface = nullptr;
|
||||||
|
}
|
||||||
pSurfaceLevel_orig->Release();
|
pSurfaceLevel_orig->Release();
|
||||||
|
|
||||||
if (pOffscreenSurface->LockRect(&d3dlr, nullptr, D3DLOCK_READONLY) != D3D_OK) {
|
if (pOffscreenSurface->LockRect(&d3dlr, nullptr, D3DLOCK_READONLY) != D3D_OK) {
|
||||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
|
||||||
pOffscreenSurface->Release();
|
pOffscreenSurface->Release();
|
||||||
Warning("GetTextureHash(2D) Failed: LockRect (D3DPOOL_DEFAULT)\n");
|
Warning("GetTextureHash(2D) Failed: LockRect (D3DPOOL_DEFAULT)\n");
|
||||||
return {};
|
return {};
|
||||||
@@ -456,7 +508,6 @@ namespace {
|
|||||||
if (pOffscreenSurface != nullptr) {
|
if (pOffscreenSurface != nullptr) {
|
||||||
pOffscreenSurface->UnlockRect();
|
pOffscreenSurface->UnlockRect();
|
||||||
pOffscreenSurface->Release();
|
pOffscreenSurface->Release();
|
||||||
if (pResolvedSurface != nullptr) pResolvedSurface->Release();
|
|
||||||
}
|
}
|
||||||
else if (pResolvedSurface != nullptr) {
|
else if (pResolvedSurface != nullptr) {
|
||||||
pResolvedSurface->UnlockRect();
|
pResolvedSurface->UnlockRect();
|
||||||
|
|||||||
+30
-5
@@ -2,10 +2,11 @@
|
|||||||
#include <Psapi.h>
|
#include <Psapi.h>
|
||||||
#include "MinHook.h"
|
#include "MinHook.h"
|
||||||
#include "D3D9Hooks.h"
|
#include "D3D9Hooks.h"
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
import TextureClient;
|
import TextureClient;
|
||||||
|
|
||||||
void ExitInstance();
|
void ExitInstance(bool is_unloading);
|
||||||
void InitInstance(HINSTANCE hModule);
|
void InitInstance(HINSTANCE hModule);
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -179,8 +180,6 @@ HRESULT APIENTRY Direct3DCreate9Ex(UINT SDKVersion, IDirect3D9Ex** ppD3D)
|
|||||||
|
|
||||||
BOOL WINAPI DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
BOOL WINAPI DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
|
||||||
{
|
{
|
||||||
UNREFERENCED_PARAMETER(lpReserved);
|
|
||||||
|
|
||||||
switch (ul_reason_for_call) {
|
switch (ul_reason_for_call) {
|
||||||
case DLL_PROCESS_ATTACH: {
|
case DLL_PROCESS_ATTACH: {
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
@@ -196,7 +195,12 @@ BOOL WINAPI DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserv
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DLL_PROCESS_DETACH: {
|
case DLL_PROCESS_DETACH: {
|
||||||
ExitInstance();
|
// Process exit (lpReserved != nullptr): other threads are gone and the OS
|
||||||
|
// reclaims everything, so touching MinHook/the device here only risks a hang.
|
||||||
|
if (lpReserved != nullptr)
|
||||||
|
break;
|
||||||
|
// FreeLibrary unload: the device is still live, so tear down cleanly.
|
||||||
|
ExitInstance(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: break;
|
default: break;
|
||||||
@@ -248,6 +252,10 @@ void InitInstance(HINSTANCE hModule)
|
|||||||
if (GetProcAddress_fn) {
|
if (GetProcAddress_fn) {
|
||||||
MH_CreateHook(GetProcAddress_fn, OnGetProcAddress, (void**)&GetProcAddress_ret);
|
MH_CreateHook(GetProcAddress_fn, OnGetProcAddress, (void**)&GetProcAddress_ret);
|
||||||
MH_EnableHook(GetProcAddress_fn);
|
MH_EnableHook(GetProcAddress_fn);
|
||||||
|
// Re-JIT the patched prologue + trampoline under the x86-on-ARM64 emulator (no-op native).
|
||||||
|
const HANDLE proc = GetCurrentProcess();
|
||||||
|
FlushInstructionCache(proc, (void*)GetProcAddress_fn, 64);
|
||||||
|
if (GetProcAddress_ret) FlushInstructionCache(proc, (void*)GetProcAddress_ret, 64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,13 +322,30 @@ extern "C" __declspec(dllexport) int __cdecl GetFiles(wchar_t* buffer, const siz
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExitInstance()
|
// Optional clean-shutdown entry: call this from the host (on a normal thread) BEFORE
|
||||||
|
// FreeLibrary so teardown runs off the loader lock, where MinHook can safely suspend
|
||||||
|
// the render thread. A later FreeLibrary then unloads with nothing left to undo.
|
||||||
|
extern "C" __declspec(dllexport) void __cdecl Shutdown()
|
||||||
{
|
{
|
||||||
|
ExitInstance(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExitInstance(bool is_unloading)
|
||||||
|
{
|
||||||
|
// Teardown must run exactly once, whether reached via Shutdown() or DllMain detach.
|
||||||
|
static std::atomic<bool> torn_down{false};
|
||||||
|
if (torn_down.exchange(true)) return;
|
||||||
|
|
||||||
DISABLE_HOOK(GetProcAddress_fn);
|
DISABLE_HOOK(GetProcAddress_fn);
|
||||||
|
|
||||||
// Revert every D3D9 vtable hook so the original objects are left pristine.
|
// Revert every D3D9 vtable hook so the original objects are left pristine.
|
||||||
RemoveAllD3D9Hooks();
|
RemoveAllD3D9Hooks();
|
||||||
|
|
||||||
|
// On a real unload the device is still alive; release our replacement textures.
|
||||||
|
if (is_unloading) {
|
||||||
|
DestroyAllTextureClients();
|
||||||
|
}
|
||||||
|
|
||||||
MH_Uninitialize();
|
MH_Uninitialize();
|
||||||
|
|
||||||
if (gMod_Loaded_d3d9_Module_Handle)
|
if (gMod_Loaded_d3d9_Module_Handle)
|
||||||
|
|||||||
Reference in New Issue
Block a user