diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6c2e952
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,64 @@
+# Prerequisites
+*.d
+
+# Object files
+*.o
+*.ko
+*.obj
+*.elf
+
+# Linker output
+*.ilk
+*.map
+*.exp
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Debug files
+*.dSYM/
+*.su
+*.idb
+*.pdb
+
+# Kernel Module Compile Results
+*.mod*
+*.cmd
+.tmp_versions/
+modules.order
+Module.symvers
+Mkfile.old
+dkms.conf
+
+# IDE
+dist/
+.vscode
+infzoom/.vscode
+infzoom/.vs
+infzoom/Debug
+infzoom/Release
+infzoom/x64
+infzoom/x86
+
+infzoom.log
diff --git a/README.md b/README.md
index e0ea776..8a1bc24 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,48 @@
-# inf_launch_ext
-INFINITASの起動オプションをいじる
-
-For USTA(Korean) Version:
-[Coldlapse/inf_launch_ext_kr](https://github.com/Coldlapse/inf_launch_ext_kr)
-
-## 注意
-このスクリプトを使用することによって生じる問題等の責任を一切負いません
-このスクリプトを作成するにあたってゲーム本体の改造やリバースエンジニアリング等の利用規約違反となる行為は行っていませんが、なにがあるかわからないので自己責任にて使用してください。
-なお、作者及びこのスクリプトはKonami Amusementと一切関係ありません。
-
-これはランチャーの代わりにこのスクリプトを立ち上げるようにレジストリを変更します。
-**スクリプトファイルを削除する前に必ずアンインストール方法をお読みください。**
-
-## なにこれ
-INFINITASの起動ページからランチャーではなくこのスクリプトを起動するようにし、コマンドラインオプションを付け加えてゲームを起動できるようにするツールです。
-
-できること
-- 通常のランチャーの起動
-- ランチャーを介さずゲーム本体を起動
-- ASIO出力やウィンドウモードを使用して起動
-- ウィンドウサイズの変更、ボーダーレス化
-- 前回の設定の読み込み
-
-## ASIO出力について
-ARESPEARに採用されていることでも知られるASUS Xonar AEならこのスクリプトでASIOを使うことができます。WASAPI排他モードを超える低遅延や安定性を期待できそうです。
-それ以外でもある条件を満たせば使えます。詳しくは[ASIOについて](https://github.com/darekasan/inf_launch_ext/blob/master/asio.md)を参照。
-
-## インストール
-#### 1 [inf_launch_ext.ps1](https://github.com/darekasan/inf_launch_ext/blob/master/inf_launch_ext.ps1)をダウンロード
-どこでもいいです。
-#### 2 PowerShellを管理者として開く
-ダウンロードしたフォルダで「ファイル > Windows PowerShellを開く > Windows PowerShellを管理者として開く」とするとそのフォルダがカレントディレクトリになるので楽です。
-![エクスプローラーの左上](https://raw.githubusercontent.com/darekasan/inf_launch_ext/master/doc_img/explorer.png)
-※「ダウンロード」「デスクトップ」「ドキュメント」の直下では選択できないことがあるので、その場合はフォルダを作成してそこに入れるなどして対処しましょう。もちろん、別の方法でPowerShellを立ち上げて実行してもOKです。
-#### 3 PowerShell実行ポリシーを変更(スクリプトの実行に必要です)
+# infzoom
+
+infzoom is a custom launcher for beatmania IIDX Infinitas.
+
+## Disclaimers
+
+infzoom is based on [darekasan/inf_launch_ext](https://github.com/darekasan/inf_launch_ext).
+
+infzoom will use functionalities built into Windows to change window decorations and resize the game window. None of this is invasive and very unlikely that any of this would be considered a violation of Terms of Service / EULA of Infinitas.
+
+That being said, contributors of this project (or the fork this project is based on) are **not** responsible for any consequences of using this project, which can include (but not limited to) getting banned from the game service.
+
+Use at your own risk.
+
+## Features
+
+The launcher provides the following options:
+
+ 1. Skipping the updater and launching the game directly
+ 1. Ability to use ASIO audio output, instead of built-in WASAPI
+ 1. Windowed mode (resizable)
+ 1. Ability to pan and zoom in/out, with presets.
+
+It should work with both Japanese and Korean versions. It has been tested primarily with the Japanese version.
+
+## Installation
+
+#### 1 Go to [Releases](https://github.com/kinetic-flow/infzoom/releases) page and download the latest version.
+
+#### 2 Open a PowerShell window as administrator
+
+#### 3 Change PowerShell execution policy
```
-PS C:\Users\darekasan\Downloads> Set-ExecutionPolicy Bypass
+PS> Set-ExecutionPolicy Bypass
```
-※Bypassにしたくない人はダウンロードしたあとにスクリプトファイルのプロパティを開いて、「このファイルへのアクセスはブロックされます」を「許可する」にチェックを入れてあげればRemoteSignedでも動きます。
-#### 4 スクリプトを実行
+This is required to execute the downloaded PowerShell script.
+
+#### 4 Run the script and install it
+
+For Japanese version, run inf_launch_ext.ps1.
+
+For Korean Infinitas service by USTA, run inf_launch_ext_kr.ps1.
+
```
-PS C:\Users\darekasan\Downloads> .\inf_launch_ext.ps1
+PS> .\inf_launch_ext.ps1
currently command: "C:\Games\beatmania IIDX INFINITAS\\launcher\modules\bm2dx_launcher.exe" "%1"
script path: C:\Users\darekasan\Downloads\inf_launch_ext.ps1
@@ -51,51 +53,89 @@ game path: C:\Games\beatmania IIDX INFINITAS\
3 : copy script file to game directory and set to new script path (recommended)
number->:
```
-「3」と入力してEnter。done.と表示されたら閉じます
-スクリプトファイルがINFINITASのインストール先にコピーされ、bm2dxinfのURLスキームのコマンドがランチャーではなくこのスクリプトに変更されます。
-## 使用方法
-いつもどおりWebページから起動すると、オプション選択画面が表示されるので数字を入力してEnterキーを押します。
-アップデートが必要なときは0を選んでランチャーを起動します。
+Here, type 3, and press enter; you are done.
+
+Once this is done, when you try to launch Infinitas from the website, it will automatically launch this custom launcher instead of the game.
+
+## Running the game
+
+When you log into Infinitas website in the browser and launch the game, you will be shown the following prompt instead of the game launcher:
+
```
+Executing: inf_launch_ext.ps1
+JP eamusement mode
Please select option.
-0 : Launcher
-1 : Normal
-2 : Normal + window mode
+0 : Launcher (required for game updates)
+1 : WASAPI
+2 : WASAPI + window mode
3 : ASIO
4 : ASIO + window mode
-number(last time: 2):
+5 : WASAPI + fullscreen borderless with zoom
+6 : ASIO + fullscreen borderless with zoom
+number(press enter for option 0):
```
-なにも指定しないでEnterを押すと前回起動時のオプションで起動します。
-もし、bm2dx.exeのプロパティの互換性タブで「管理者としてこのプログラムを実行する」にチェックを入れているなら外してください。うまく動きません。
-### ウィンドウモードの使い方 ###
-ウィンドウモードでは実行中にウィンドウの位置やサイズの変更、ボーダーレス化などの設定が行えます。
-120Hzで出力可能なモニターを使っている場合(144Hzなども含む)はWindowsのディスプレイ設定でリフレッシュレートを「120Hz」にしたほうがよさげです。
-```
-window mode setting
-example:
-window size -> 1280x720
-window position -> 100,100
-Press enter key to switch to Borderless window.
- : 854x480
- : 10,10
- :
-```
-ウィンドウのサイズや位置の指定はそれぞれ「1280x720」「100,100」といった書式で行うことができます。
-何も入力せずEnterを押すとボーダーレスウィンドウになります。これらの設定は次回起動時に引き継がれます。
-ボーダーレスウィンドウをモニターの解像度と同じ大きさにすると「排他じゃないイマドキのフルスクリーン」にできていい感じ。
+### 0 : Launcher
+
+Launches the original Infinitas launcher, which lets you change settings and update the game. You should run this once in a while to check for updates, since other options bypass the update check.
+
+### 1 : WASAPI
+
+Run the game right away, with WASAPI audio (the default audio mode).
+
+### 2 : WASAPI + window mode
+
+Same as #2, but launch the game in a resizable window.
+
+### 3 : ASIO
+
+Run the game right away, with ASIO audio.
+
+Making the game use ASIO audio requres extra steps - see [this link](https://github.com/darekasan/inf_launch_ext/blob/master/asio.md).
+
+### 4 : ASIO + window mode
+
+Same as #3, but launch the game in a resizable window.
+
+### 5 : WASAPI + fullscreen borderless with zoom
+
+Launch the game with WASAPI audio with "borderless fullscreen" window. It also runs infzoom.exe which give you the ability to resize the window with hotkeys.
+
+To change settings, like which monitor is used, modify the zoom presets, or to change the hotkeys: modify **infzoom.ini** in the game directory.
+
+The default hotkeys are:
+
+* Zoom into 1P: **F1**
+* Zoom into 2P: **F2**
+* Zoom into DP: **F3**
+* Reset zoom & enable manual mode: **F5**
+* In manual mode, move window: **ctrl + up, down, left, or right**
+* In manual mode, change zoom: **alt + up, down, left, or right**
+* Forcibly close game: **alt+ F10****
+
+### 6 :ASIO + fullscreen borderless with zoom
+
+Same as #5, but with ASIO audio.
+
+## Zoom mode: important notes
+
+This application allows the game window to stretch beyond the visible desktop area, like this:
+
+![window size demo](https://raw.githubusercontent.com/kinetic-flow/infzoom/master/doc_img/monitor.png)
+
+... which effectively gives you the "zoom in" function.
+
+A side effect of this is that, if you have multiple monitors, the game window may spill over to other windows.
+
+Running the game in windowed mode is not originally intended to be possible. You may or may not experience sync issues. Ensure you are launching the game with the correct refresh rate.
+
+The game window is scaled without any filters, so it can look pixelated. There is not much that can be done about that, but you can work around this by changing the Windows desktop size to be something smaller (maybe 1280x720) and launching the game.
+
+## Troubleshooting
-## アンインストール
-**この手順を踏まずにスクリプトファイルを削除してしまうとランチャーが起動できなくなって詰みます。**
-「3 : copy script file to game directory and set to new script path (recommended)」を選んでインストールした場合はINFINITASのインストール先(C:\Games\beatmania IIDX INFINITASとか)にスクリプトファイルがあるはずです。
-管理者権限のPowerShellでスクリプトを実行して、「0 : revert to default」を選ぶとレジストリを元に戻します。
-それが済んだらinf_launch_ext.ps1とconfig.jsonを削除しても大丈夫です。
+Look for infzoom.log in the game directory.
-※どうにもならなくなったときはお手数ですがINFINITASを再インストールしてください。
+## Uninstallation
-## 既知の問題
-- ウィンドウサイズを縦横整数倍以外にすると汚い(補間処理がないため)
-補間処理を入れようと思うと間に挟まってレンダリング結果横取りして別のウィンドウに表示するくらいしか方法が無いので、気になる方は~~DxWndとかで窓化したほうがいいです。~~ DxWndは64bitバイナリ非対応なので新INFINITASじゃ使えないんですね
-- 導入がむずい
-「何をやってるのか全然わからん」って人は手を出さないほうが無難?
+It is not sufficient to delete the PowerShell script. Instead, run the script again, but choose option "0 : revert to default". After that, you can delete the script.
diff --git a/deploy.bat b/deploy.bat
new file mode 100644
index 0000000..c497d75
--- /dev/null
+++ b/deploy.bat
@@ -0,0 +1,8 @@
+cd /d %~dp0
+copy /y infzoom\infzoom.ini infzoom\Debug
+copy /y infzoom\infzoom.ini infzoom\Release
+
+set outdir="G:\Games\beatmania IIDX INFINITAS"
+copy /y inf_launch_ext.ps1 %outdir%
+copy /y infzoom\infzoom.ini %outdir%
+copy /y infzoom\Debug\infzoom.exe %outdir%
\ No newline at end of file
diff --git a/doc_img/monitor.png b/doc_img/monitor.png
new file mode 100644
index 0000000..6673365
Binary files /dev/null and b/doc_img/monitor.png differ
diff --git a/inf_launch_ext.ps1 b/inf_launch_ext.ps1
index 83da73a..d0dd099 100644
--- a/inf_launch_ext.ps1
+++ b/inf_launch_ext.ps1
@@ -1,37 +1,61 @@
-# INFINITASのレジストリ インストール先の取得に使う
+Add-Type -AssemblyName System.Windows.Forms
+
+$ScriptName = [Regex]::Match(
+ $MyInvocation.InvocationName,
+ '[^\\]+\Z',
+ [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::SingleLine
+ ).Value
+
+Write-Host "Executing: $ScriptName"
+
+$ScriptIsUsta = $ScriptName -match '.*kr.ps1$'
+if ($ScriptIsUsta) {
+ Write-Host "KR USTA mode"
+ $ScriptIsUsta = $true
+} else {
+ Write-Host "JP eamusement mode"
+ $ScriptIsUsta = $false
+}
+
+# Use to get INFINITAS registry installation path
$InfRegistry = "HKLM:\SOFTWARE\KONAMI\beatmania IIDX INFINITAS"
-# ゲーム本体のパス 通常はレジストリから取得
+# Path of the game itself Usually obtained from the registry
#$InfPath = "C:\Games\beatmania IIDX INFINITAS\"
$InfPath = Get-ItemPropertyValue -LiteralPath $InfRegistry -Name "InstallDir"
$InfExe = Join-Path $InfPath "\game\app\bm2dx.exe"
$InfLauncher = Join-Path $InfPath "\launcher\modules\bm2dx_launcher.exe"
cd $InfPath | Out-Null
-# bm2dxinf:// のレジストリ
+# bm2dxinf:// registry
$InfOpen = "HKCR:bm2dxinf\shell\open\command\"
+if ($ScriptIsUsta) {
+ $InfOpen = "HKCR:bm2dx-kr\shell\open\command\"
+}
-# このスクリプトのフルパス
+# full path to this script
$ScriptPath = $MyInvocation.MyCommand.Path
-# 設定ファイル
+# setting file
$ConfigJson = Join-Path $PSScriptRoot "config.json"
-$Config = @{
+$Config = [ordered]@{
"Option"="0"
"WindowWidth"="1280"
"WindowHeight"="720"
"WindowPositionX"="0"
"WindowPositionY"="0"
"Borderless"=$false
+ "FsMonitor"="0"
}
-# ウィンドウスタイル(調べてもよくわかんなかった)
-$WSDefault = 348651520
-$WSBorderless = 335544320
+# window style
+# see https://learn.microsoft.com/en-us/windows/win32/winmsg/window-styles
+$WSDefault = 0x14CC0000
+$WSBorderless = 0x14080000
-# Win32API関数の定義
-Add-Type @"
+# Define Win32 API functions
+$Source = @"
using System;
using System.Runtime.InteropServices;
@@ -50,14 +74,16 @@ Add-Type @"
[DllImport("user32.dll")]
internal static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect);
+
+ [DllImport("User32.dll")]
+ public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
[StructLayout(LayoutKind.Sequential)]
- internal struct RECT
- {
- public int left, top, right, bottom;
+ internal struct RECT
+ {
+ public int left, top, right, bottom;
}
- // 外枠の大きさを考慮したウィンドウサイズ変更
public static void MoveWindow2(IntPtr hndl, int x, int y, int w, int h, bool isBl){
if(isBl){
MoveWindow(hndl, x, y, w, h, true);
@@ -81,15 +107,17 @@ Add-Type @"
}
}
-
}
"@
+Add-Type -TypeDefinition $Source -Language CSharp
function Save-Config() {
$Config | ConvertTo-Json | Out-File -FilePath $ConfigJson -Encoding utf8
}
function Start-Exe($exe, $workDir, $arg){
+ Write-Host "Start-Exe launching:`n EXE: $exe`n ARG: $arg`n DIR: $workDir`n"
+
$info = New-Object System.Diagnostics.ProcessStartInfo
$info.FileName = $exe
$info.WorkingDirectory = $workDir
@@ -98,9 +126,8 @@ function Start-Exe($exe, $workDir, $arg){
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $info
-
- $p.Start() | Out-Null
+ $p.Start() | Out-Null
return $p
}
@@ -112,8 +139,34 @@ function Switch-Borderless($isBl){
}
}
+function Get-Monitor($monitor_number){
+ $monitor_number = [int]$monitor_number
+ # https://stackoverflow.com/questions/7967699/get-screen-resolution-using-wmi-powershell-in-windows-7
+ # get a lit of monitors...
+ $screens = [system.windows.forms.screen]::AllScreens
+
+ # find primary monitor first
+ $primary = $screens[0]
+ $col_screens | ForEach-Object {
+ if ("$($_.Primary)" -eq "True") {
+ $primary = $_
+ break
+ }
+ }
+
+ # 0 = primary, 1 and up = monitor number
+ if ($monitor_number -eq 0) {
+ return $primary
+ }
+
+ if ($monitor_number -gt $screens.Count) {
+ return $primary
+ }
+
+ return $screens[$monitor_number - 1]
+}
-# 引数を指定しなかったときにレジストリ変更
+# change registry when no argument is specified
if ([string]::IsNullOrEmpty($Args[0])) {
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | Out-Null
$val = Get-ItemPropertyValue -LiteralPath $InfOpen -Name "(default)"
@@ -137,8 +190,16 @@ if ([string]::IsNullOrEmpty($Args[0])) {
$val = """powershell"" ""-file"" ""${ScriptPath}"" ""%1"""
}
3 {
- $NewScriptPath = Join-Path $InfPath "inf_launch_ext.ps1"
- Copy-Item $ScriptPath $NewScriptPath
+ $From = Join-Path $PSScriptRoot $ScriptName
+ Copy-Item -Path $From -Destination $InfPath
+
+ $From = Join-Path $PSScriptRoot "infzoom.exe"
+ Copy-Item -Path $From -Destination $InfPath
+
+ $From = Join-Path $PSScriptRoot "infzoom.ini"
+ Copy-Item -Path $From -Destination $InfPath
+
+ $NewScriptPath = Join-Path $InfPath $ScriptName
$val = """powershell"" ""-file"" ""${NewScriptPath}"" ""%1"""
}
Default {
@@ -151,38 +212,40 @@ if ([string]::IsNullOrEmpty($Args[0])) {
exit
}
-# ゲームを起動するためのもの ここから
-# 設定ファイルを読み込む
+# Something to start the game from here
+# read configuration file
if(Test-Path $ConfigJson){
- $Config = @{}
+ $Config = [ordered]@{}
(ConvertFrom-Json (Get-Content $ConfigJson -Encoding utf8 -Raw )).psobject.properties | Foreach { $Config[$_.Name] = $_.Value }
}
-
-# ゲーム本体に渡す引数
+# Arguments to pass to the game itself
$InfArgs = ""
-# 引数からトークンを拾う
+# pick up the token from the arguments
$Args[0] -match "tk=(.{64})" | Out-Null
$InfArgs += " -t "+$Matches[1]
-# トライアルモードなら--trialをつける
+# add --trial for trial mode
if ($Args[0].Contains("trial")) {
$InfArgs += " --trial"
}
echo "Please select option."
-echo "0 : Launcher"
-echo "1 : Normal"
-echo "2 : Normal + window mode"
+echo "0 : Launcher (required for game updates)"
+echo "1 : WASAPI"
+echo "2 : WASAPI + window mode"
echo "3 : ASIO"
echo "4 : ASIO + window mode"
+echo "5 : WASAPI + fullscreen borderless with zoom"
+echo "6 : ASIO + fullscreen borderless with zoom"
-$num = Read-Host "number(last time: $($Config["Option"]))"
+$num = Read-Host "number(press enter for option $($Config["Option"]))"
if([string]::IsNullOrEmpty($num)){
$num=$Config["Option"]
}
+$FullScreenBorderlessWithZoom = $false
switch ($num) {
0 {
Start-Process -FilePath $InfLauncher -ArgumentList $Args[0]
@@ -201,62 +264,92 @@ switch ($num) {
$InfArgs += " -w"
$InfArgs += " --asio"
}
+ 5 {
+ $InfArgs += " -w"
+ $FullScreenBorderlessWithZoom = $true
+ }
+ 6 {
+ $InfArgs += " -w"
+ $InfArgs += " --asio"
+ $FullScreenBorderlessWithZoom = $true
+ }
Default {
exit
}
}
-# 設定を保存
+if ($ScriptIsUsta) {
+ $InfArgs += " --kr"
+}
+
+# save settings
$Config["Option"] = [string]$num
Save-Config
-# INFINITASを起動
-$p = Start-Exe($InfExe,"",""""+$InfArgs+"""")
+# start INFINITAS
+$p = Start-Exe $InfExe "" $InfArgs
-# ウィンドウモードのとき
-if($InfArgs.Contains("-w")){
- # ウィンドウ作成まで待つ
- $p.WaitForInputIdle() | Out-Null
+# in window mode...
+if ($InfArgs.Contains("-w")){
- # ウィンドウハンドルの取得
+ # wait for window creation
+ $p.WaitForInputIdle() | Out-Null
$handle = $p.MainWindowHandle
- # 前回の位置と大きさにする
- Switch-Borderless($Config["Borderless"])
- [Win32Api]::MoveWindow2($handle, $Config["WindowPositionX"], $Config["WindowPositionY"], $Config["WindowWidth"], $Config["WindowHeight"], $Config["Borderless"])
+ if ($FullScreenBorderlessWithZoom) {
+ # we let the separate EXE handle everything for this mode
+ $InfZoomExe = Join-Path $PSScriptRoot "infzoom.exe"
+ $infzoom_p = Start-Exe $InfZoomExe $PSScriptRoot $handle
+ $infzoom_p.WaitForExit() | Out-Null
+ Pause
- echo ""
- echo "window mode setting"
- echo "example:"
- echo "window size -> 1280x720"
- echo "window position -> 100,100"
- echo "Press enter key to switch to Borderless window."
-
- while($true){
- $inputStr=Read-Host " "
- if([string]::IsNullOrEmpty($inputStr)){
- $Config["Borderless"] = (-Not $Config["Borderless"])
- }elseif($inputStr.Contains("x")){
- $val = $inputStr.Split('x')
- $Config["WindowWidth"]=$val[0]
- $Config["WindowHeight"]=$val[1]
- }elseif($inputStr.Contains(",")){
- $val = $inputStr.Split(',')
- $Config["WindowPositionX"]=$val[0]
- $Config["WindowPositionY"]=$val[1]
- }
+ } else {
- # ボーダーレス化
+ # set to previous position and size
Switch-Borderless($Config["Borderless"])
+ [Win32Api]::MoveWindow2(
+ $handle,
+ $Config["WindowPositionX"],
+ $Config["WindowPositionY"],
+ $Config["WindowWidth"],
+ $Config["WindowHeight"],
+ $Config["Borderless"])
+
+ echo ""
+ echo "window mode setting"
+ echo "example:"
+ echo " window size -> type 1280x720"
+ echo " window position -> type 100,100"
+ echo "Press enter key to switch to Borderless window, or use mouse cursor to resize window"
+
+ while($true){
+ $inputStr=Read-Host " "
+ if([string]::IsNullOrEmpty($inputStr)){
+ $Config["Borderless"] = (-Not $Config["Borderless"])
+ }elseif($inputStr.Contains("x")){
+ $val = $inputStr.Split('x')
+ $Config["WindowWidth"]=$val[0]
+ $Config["WindowHeight"]=$val[1]
+ }elseif($inputStr.Contains(",")){
+ $val = $inputStr.Split(',')
+ $Config["WindowPositionX"]=$val[0]
+ $Config["WindowPositionY"]=$val[1]
+ }
- # 位置とサイズを反映
- [Win32Api]::MoveWindow2($handle, $Config["WindowPositionX"], $Config["WindowPositionY"], $Config["WindowWidth"], $Config["WindowHeight"], $Config["Borderless"])
+ # make borderless
+ Switch-Borderless($Config["Borderless"])
- # 設定ファイルに書き込む
- Save-Config
+ # Reflect position and size
+ [Win32Api]::MoveWindow2(
+ $handle,
+ $Config["WindowPositionX"],
+ $Config["WindowPositionY"],
+ $Config["WindowWidth"],
+ $Config["WindowHeight"],
+ $Config["Borderless"])
+
+ # write to config file
+ Save-Config
+ }
}
}
-
-
-
-
diff --git a/infzoom.sln b/infzoom.sln
new file mode 100644
index 0000000..4a84743
--- /dev/null
+++ b/infzoom.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.32414.318
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "infzoom", "infzoom.vcxproj", "{BF3C8448-6170-43FB-824D-EC7F96334D9D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x86 = Debug|x86
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Debug|x86.ActiveCfg = Debug|Win32
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Debug|x86.Build.0 = Debug|Win32
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Release|x86.ActiveCfg = Release|Win32
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Release|x86.Build.0 = Release|Win32
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {AEC958AA-F6DA-4E1A-82B3-4CD01D281180}
+ EndGlobalSection
+EndGlobal
diff --git a/infzoom/external/ini.h b/infzoom/external/ini.h
new file mode 100644
index 0000000..239906a
--- /dev/null
+++ b/infzoom/external/ini.h
@@ -0,0 +1,684 @@
+/* ini.h - simple single-header ini parser in c99
+
+ in just one c/c++ file do this:
+ #define INI_IMPLEMENTATION
+ #include "ini.h"
+
+ license:
+ see end of file
+
+ options:
+ by default, when there are multiple tables with the same name, the
+ parser simply keeps adding to the tables list, it wastes memory
+ but it is also much faster, especially for bigger files.
+ if you want the parser to merge all the tables together use the
+ following option:
+ - merge_duplicate_tables
+ when adding keys, if a table has two same keys, the parser
+ adds it to the table anyway, meaning that when it searches for the
+ key with ini_get it will get the first one but it will still have
+ both in table.values, to override this behaviour and only keep the
+ last value use:
+ - override_duplicate_keys
+ the default key/value divider is '=', if you want to change is use
+ - key_value_divider
+
+ usage:
+ - simple file:
+ ini_t ini = ini_parse("file.ini", NULL);
+
+ char *name = ini_as_str(ini_get(ini_get_table(&ini, INI_ROOT), "name"), false);
+
+ initable_t *server = ini_get_table(&ini, "server");
+ int port = (int)ini_as_int(ini_get(server, "port"));
+ bool use_threads = ini_as_bool(ini_get(server, "use threads"));
+
+ ini_free(&ini);
+ free(name);
+
+ - string:
+ const char *ini_str =
+ "name : web-server\n"
+ "[server]\n"
+ "port : 8080\n"
+ "ip : localhost\n"
+ "use threads : false";
+ ini_t ini = ini_parse_str(ini_str, &(iniopts_t){ .key_value_divider = ':' });
+
+ char *name = ini_as_str(ini_get(ini_get_table(&ini, INI_ROOT), "name"), false);
+
+ initable_t *server = ini_get_table(&ini, "server");
+ int port = (int)ini_as_int(ini_get(server, "port"));
+ bool use_threads = ini_as_bool(ini_get(server, "use threads"));
+ char ip[64];
+ int iplen = ini_to_str(ini_get(server, "ip"), ip, sizeof(ip), false);
+ if (iplen < 0) {
+ printf("(err) couldn't get ip: %s\n", ini_explain(iplen));
+ }
+
+ free(name);
+ ini_free(&ini);
+*/
+
+#ifndef INI_LIB_HEADER
+#define INI_LIB_HEADER
+
+#define _CRT_SECURE_NO_WARNINGS
+#include
+#include
+#include // SIZE_MAX
+#include
+#include
+
+#define inivec_t(T) T *
+
+#define ivec_free(vec) ((vec) ? free(ini__vec_header(vec)), NULL : NULL)
+#define ivec_copy(src, dest) (ivec_free(dest), ivec_reserve(dest, ivec_len(src)), memcpy(dest, src, ivec_len(src)))
+
+#define ivec_push(vec, ...) (ini__vec_may_grow(vec, 1), (vec)[ini__vec_len(vec)] = (__VA_ARGS__), ini__vec_len(vec)++)
+#define ivec_rem(vec, ind) ((vec) ? (vec)[(ind)] = (vec)[--ini__vec_len(vec)], NULL : NULL)
+#define ivec_rem_it(vec, it) ivec_rem((vec), (it)-(vec))
+#define ivec_len(vec) ((vec) ? ini__vec_len(vec) : 0)
+#define ivec_cap(vec) ((vec) ? ini__vec_cap(vec) : 0)
+
+#define ivec_end(vec) ((vec) ? (vec) + ini__vec_len(vec) : NULL)
+#define ivec_back(vec) ((vec)[ini__vec_len(vec) - 1])
+
+#define ivec_add(vec, n) (ini__vec_may_grow(vec, (n)), ini__vec_len(vec) += (unsigned int)(n), &(vec)[ini__vec_len(vec)-(n)])
+#define ivec_reserve(vec, n) (ini__vec_may_grow(vec, (n)))
+
+#define ivec_clear(vec) ((vec) ? ini__vec_len(vec) = 0 : 0)
+#define ivec_pop(vec) ((vec)[--ini__vec_len(vec)])
+
+typedef struct {
+ const char* buf;
+ size_t len;
+} inistrv_t;
+
+typedef struct {
+ inistrv_t key;
+ inistrv_t value;
+} inivalue_t;
+
+typedef struct {
+ inistrv_t name;
+ inivec_t(inivalue_t) values;
+} initable_t;
+
+typedef struct {
+ char* text;
+ inivec_t(initable_t) tables;
+} ini_t;
+
+typedef struct {
+ bool merge_duplicate_tables; // default: false
+ bool override_duplicate_keys; // default: false
+ char key_value_divider; // default: =
+} iniopts_t;
+
+typedef enum {
+ INI_NO_ERR = 0,
+ INI_INVALID_ARGS = -1,
+ INI_BUFFER_TOO_SMALL = -2,
+} inierr_t;
+
+#define INI_ROOT NULL
+
+// parses a ini file, if options is NULL it uses the default options
+ini_t ini_parse(const char* filename, const iniopts_t* options);
+// parses a ini string, if options is NULL it uses the default options
+ini_t ini_parse_str(const char* ini_str, const iniopts_t* options);
+// parses a ini buffer, this buffer *can* contain '\0', if options is NULL it uses the default options
+ini_t ini_parse_buf(const char* buf, size_t buflen, const iniopts_t* options);
+// parses a ini file from a file descriptor, if options is NULL it uses the default options
+ini_t ini_parse_fp(FILE* fp, const iniopts_t* options);
+// checks that the ini file has been parsed correctly
+bool ini_is_valid(ini_t* ctx);
+void ini_free(ini_t* ctx);
+
+// return a table with name , returns NULL if nothing was found
+initable_t* ini_get_table(ini_t* ctx, const char* name);
+// return a value with key , returns NULL if nothing was found or if is NULL
+inivalue_t* ini_get(initable_t* ctx, const char* key);
+
+// returns an allocated vector of values divided by
+// if is 0 then it defaults to ' ', must be freed with ivec_free
+inivec_t(inistrv_t) ini_as_array(const inivalue_t* value, char delim);
+unsigned long long ini_as_uint(const inivalue_t* value);
+long long ini_as_int(const inivalue_t* value);
+double ini_as_num(const inivalue_t* value);
+bool ini_as_bool(const inivalue_t* value);
+// copies a value into a c string, must be freed
+// copies the value to an allocated c string, if remove_escape_chars is true
+// it also removes escape characters from a string (e.g. \; or \#)
+// returns the allocated string
+char* ini_as_str(const inivalue_t* value, bool remove_escape_chars);
+
+// divides the inivalue_t by and puts the resulting values in
+// returns the number of items on success or <0 on failure (check inierr_t)
+inierr_t ini_to_array(const inivalue_t* value, inistrv_t* arr, size_t len, char delim);
+// copies the value to a buffer, buflen includes the null character,
+// if remove_escape_chars is true it also removes escape characters from a string (e.g. \; or \#)
+// returns the number of characters written on succes or <0 on failure (check inierr_t)
+inierr_t ini_to_str(const inivalue_t* value, char* buf, size_t buflen, bool remove_escape_chars);
+
+// returns a human readable version of
+const char* ini_explain(inierr_t error);
+
+#endif
+
+#ifdef INI_IMPLEMENTATION
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef __cplusplus
+#define CDECL(type) type
+#else
+#define CDECL(type) (type)
+#endif
+
+#define ini__vec_header(vec) ((unsigned int *)(vec) - 2)
+#define ini__vec_cap(vec) ini__vec_header(vec)[0]
+#define ini__vec_len(vec) ini__vec_header(vec)[1]
+
+#define ini__vec_need_grow(vec, n) ((vec) == NULL || ini__vec_len(vec) + n >= ini__vec_cap(vec))
+#define ini__vec_may_grow(vec, n) (ini__vec_need_grow(vec, (n)) ? ini__vec_grow(vec, (unsigned int)(n)) : (void)0)
+#define ini__vec_grow(vec, n) ini__vec_grow_impl((void **)&(vec), (n), sizeof(*(vec)))
+
+inline static void ini__vec_grow_impl(void** arr, unsigned int increment, unsigned int itemsize) {
+ int newcap = *arr ? 2 * ini__vec_cap(*arr) + increment : increment + 1;
+ void* ptr = realloc(*arr ? ini__vec_header(*arr) : 0, itemsize * newcap + sizeof(unsigned int) * 2);
+ assert(ptr);
+ if (ptr) {
+ if (!*arr) ((unsigned int*)ptr)[1] = 0;
+ *arr = (void*)((unsigned int*)ptr + 2);
+ ini__vec_cap(*arr) = newcap;
+ }
+}
+
+static const iniopts_t ini__default_opts = {
+ false, // merge_duplicate_tables
+ false, // override_duplicate_keys
+ '=', // key_value_divider
+};
+
+typedef struct {
+ const char* start;
+ const char* cur;
+ size_t len;
+} ini__istream_t;
+
+static ini_t ini__parse_internal(char* text, size_t textlen, const iniopts_t* options);
+static char* ini__read_whole_file(FILE* fp, size_t* filelen);
+static iniopts_t ini__set_default_opts(const iniopts_t* options);
+static initable_t* ini__find_table(ini_t* ctx, inistrv_t name);
+static inivalue_t* ini__find_value(initable_t* table, inistrv_t key);
+static void ini__add_table(ini_t* ctx, ini__istream_t* in, const iniopts_t* options);
+static void ini__add_value(initable_t* table, ini__istream_t* in, const iniopts_t* options);
+static char* ini__strdup(const char* src, size_t len);
+static int ini__rem_escaped(inistrv_t value, char* buf, size_t buflen);
+
+// string stream helper functions
+static ini__istream_t istr__init(const char* str, size_t len);
+static bool istr__is_finished(ini__istream_t* in);
+static void istr__skip_whitespace(ini__istream_t* in);
+static void istr__ignore(ini__istream_t* in, char delim);
+static void istr__skip(ini__istream_t* in);
+static inistrv_t istr__get_view(ini__istream_t* in, char delim);
+
+// string view helper functions
+static inistrv_t strv__from_str(const char* str);
+static inistrv_t strv__trim(inistrv_t view);
+static inistrv_t strv__sub(inistrv_t view, size_t from, size_t to);
+static bool strv__is_empty(inistrv_t view);
+static int strv__cmp(inistrv_t a, inistrv_t b);
+
+ini_t ini_parse(const char* filename, const iniopts_t* options) {
+ if (!filename) return CDECL(ini_t) { 0 };
+ FILE* fp = fopen(filename, "rb");
+ size_t filelen = 0;
+ char* file_data = ini__read_whole_file(fp, &filelen);
+ fclose(fp);
+ return ini__parse_internal(file_data, filelen, options);
+}
+
+ini_t ini_parse_str(const char* ini_str, const iniopts_t* options) {
+ size_t ini_str_len = strlen(ini_str);
+ return ini__parse_internal(ini__strdup(ini_str, ini_str_len), ini_str_len, options);
+}
+
+ini_t ini_parse_buf(const char* buf, size_t buflen, const iniopts_t* options) {
+ return ini__parse_internal(ini__strdup(buf, buflen), buflen, options);
+}
+
+ini_t ini_parse_fp(FILE* fp, const iniopts_t* options) {
+ size_t filelen = 0;
+ char* file_data = ini__read_whole_file(fp, &filelen);
+ return ini__parse_internal(file_data, filelen, options);
+}
+
+bool ini_is_valid(ini_t* ctx) {
+ return ctx && ctx->text != NULL;
+}
+
+void ini_free(ini_t* ctx) {
+ if (!ctx) return;
+ free(ctx->text);
+ for (initable_t* tab = ctx->tables; tab != ivec_end(ctx->tables); ++tab) {
+ ivec_free(tab->values);
+ }
+ ivec_free(ctx->tables);
+ *ctx = (ini_t){ 0 };
+}
+
+initable_t* ini_get_table(ini_t* ctx, const char* name) {
+ if (!name) return ctx->tables;
+ else return ini__find_table(ctx, strv__from_str(name));
+}
+
+inivalue_t* ini_get(initable_t* ctx, const char* key) {
+ return ctx ? ini__find_value(ctx, strv__from_str(key)) : NULL;
+}
+
+inivec_t(inistrv_t) ini_as_array(const inivalue_t* value, char delim) {
+ if (!value) return NULL;
+ if (strv__is_empty(value->value)) return 0;
+ if (!delim) delim = ' ';
+
+ inivec_t(inistrv_t) out = NULL;
+ inistrv_t v = value->value;
+
+ size_t start = 0;
+ for (size_t i = 0; i < v.len; ++i) {
+ if (v.buf[i] == delim) {
+ inistrv_t arr_val = strv__trim(strv__sub(v, start, i));
+ if (!strv__is_empty(arr_val)) {
+ ivec_push(out, arr_val);
+ }
+ start = i + 1;
+ }
+ }
+ inistrv_t last = strv__trim(strv__sub(v, start, SIZE_MAX));
+ if (!strv__is_empty(last)) {
+ ivec_push(out, last);
+ }
+ return out;
+}
+
+unsigned long long ini_as_uint(const inivalue_t* value) {
+ if (!value || strv__is_empty(value->value)) return 0;
+ unsigned long long out = strtoull(value->value.buf, NULL, 0);
+ if (out == ULLONG_MAX) {
+ out = 0;
+ }
+ return out;
+}
+
+long long ini_as_int(const inivalue_t* value) {
+ if (!value || strv__is_empty(value->value)) return 0;
+ long long out = strtoll(value->value.buf, NULL, 0);
+ if (out == LONG_MAX || out == LONG_MIN) {
+ out = 0;
+ }
+ return out;
+}
+
+double ini_as_num(const inivalue_t* value) {
+ if (!value || strv__is_empty(value->value)) return 0;
+ double out = strtod(value->value.buf, NULL);
+ if (out == HUGE_VAL || out == -HUGE_VAL) {
+ out = 0;
+ }
+ return out;
+}
+
+bool ini_as_bool(const inivalue_t* value) {
+ if (!value) return false;
+ if (value->value.len == 4 && strncmp(value->value.buf, "true", 4) == 0) {
+ return true;
+ }
+ return false;
+}
+
+char* ini_as_str(const inivalue_t* value, bool remove_escape_chars) {
+ if (!value) return NULL;
+ inistrv_t val_strv = strv__trim(value->value);
+ char* str = ini__strdup(val_strv.buf, val_strv.len);
+ if (remove_escape_chars) {
+ inistrv_t strv = { str, val_strv.len };
+ // we do +1 to include the null pointer at the end
+ ini__rem_escaped(strv, str, strv.len + 1);
+ }
+ return str;
+}
+
+inierr_t ini_to_array(const inivalue_t* value, inistrv_t* arr, size_t len, char delim) {
+ if (!value || !arr || len == 0) return INI_INVALID_ARGS;
+ if (strv__is_empty(value->value)) return 0;
+ if (!delim) delim = ' ';
+
+ inistrv_t strv = value->value;
+ int count = 0;
+ size_t start = 0;
+ for (size_t i = 0; i < strv.len; ++i) {
+ if (strv.buf[i] == delim) {
+ inistrv_t arr_val = strv__trim(strv__sub(strv, start, i));
+ if (!strv__is_empty(arr_val)) {
+ if (count >= len) {
+ return INI_BUFFER_TOO_SMALL;
+ }
+ arr[count++] = arr_val;
+ }
+ start = i + 1;
+ }
+ }
+ inistrv_t last = strv__trim(strv__sub(strv, start, SIZE_MAX));
+ if (!strv__is_empty(last)) {
+ if (count >= len) {
+ return INI_BUFFER_TOO_SMALL;
+ }
+ arr[count++] = last;
+ }
+ return count;
+}
+
+inierr_t ini_to_str(const inivalue_t* value, char* buf, size_t buflen, bool remove_escape_chars) {
+ if (!value || !buf || buflen == 0) return INI_INVALID_ARGS;
+ if (strv__is_empty(value->value)) {
+ buf[0] = '\0';
+ return 0;
+ }
+ inistrv_t strv = strv__trim(value->value);
+ if (remove_escape_chars) {
+ return ini__rem_escaped(strv, buf, buflen);
+ }
+ else {
+ if (buflen < (strv.len + 1)) return INI_BUFFER_TOO_SMALL;
+ memcpy(buf, strv.buf, strv.len);
+ buf[strv.len] = '\0';
+ return strv.len;
+ }
+}
+
+const char* ini_explain(inierr_t error) {
+ switch (error) {
+ case INI_NO_ERR: return "no error";
+ case INI_INVALID_ARGS: return "invalid arguments";
+ case INI_BUFFER_TOO_SMALL: return "buffer too small";
+ }
+ return "unknown";
+}
+
+static ini_t ini__parse_internal(char* text, size_t textlen, const iniopts_t* options) {
+ ini_t ini = { text, NULL };
+ if (!text) return ini;
+ iniopts_t opts = ini__set_default_opts(options);
+ // add root table
+ initable_t root = { CDECL(inistrv_t) { "root", 4 }, NULL };
+ ivec_push(ini.tables, root);
+ ini__istream_t in = istr__init(text, textlen);
+ while (!istr__is_finished(&in)) {
+ switch (*in.cur) {
+ case '[':
+ ini__add_table(&ini, &in, &opts);
+ break;
+ case '#': case ';':
+ istr__ignore(&in, '\n');
+ break;
+ default:
+ ini__add_value(ini.tables, &in, &opts);
+ break;
+ }
+ istr__skip_whitespace(&in);
+ }
+ return ini;
+}
+
+static char* ini__read_whole_file(FILE* fp, size_t* filelen) {
+ if (!fp) return NULL;
+ fseek(fp, 0, SEEK_END);
+ size_t len = (size_t)ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+ char* buf = (char*)malloc(len + 1);
+ size_t read = fread(buf, 1, len, fp);
+ if (read != len) {
+ free(buf);
+ fclose(fp);
+ return NULL;
+ }
+ buf[len] = '\0';
+ if (filelen) *filelen = len;
+ return buf;
+}
+
+static iniopts_t ini__set_default_opts(const iniopts_t* options) {
+ if (!options) return ini__default_opts;
+
+ iniopts_t opts = ini__default_opts;
+
+ if (options->override_duplicate_keys)
+ opts.override_duplicate_keys = options->override_duplicate_keys;
+
+ if (options->merge_duplicate_tables)
+ opts.merge_duplicate_tables = options->merge_duplicate_tables;
+
+ if (options->key_value_divider)
+ opts.key_value_divider = options->key_value_divider;
+
+ return opts;
+}
+
+static initable_t* ini__find_table(ini_t* ctx, inistrv_t name) {
+ if (strv__is_empty(name)) return NULL;
+ for (unsigned int i = 0; i < ivec_len(ctx->tables); ++i) {
+ if (strv__cmp(ctx->tables[i].name, name) == 0) {
+ return ctx->tables + i;
+ }
+ }
+ return NULL;
+}
+
+static inivalue_t* ini__find_value(initable_t* table, inistrv_t key) {
+ if (strv__is_empty(key)) return NULL;
+ for (unsigned int i = 0; i < ivec_len(table->values); ++i) {
+ if (strv__cmp(table->values[i].key, key) == 0) {
+ return table->values + i;
+ }
+ }
+ return NULL;
+}
+
+static void ini__add_table(ini_t* ctx, ini__istream_t* in, const iniopts_t* options) {
+ istr__skip(in); // skip [
+ inistrv_t name = istr__get_view(in, ']');
+ istr__skip(in); // skip ]
+
+ if (strv__is_empty(name)) return;
+
+ initable_t* table = options->merge_duplicate_tables ? ini__find_table(ctx, name) : NULL;
+ if (!table) {
+ ivec_push(ctx->tables, CDECL(initable_t){ name, NULL });
+ table = &ivec_back(ctx->tables);
+ }
+ istr__ignore(in, '\n');
+ istr__skip(in);
+ while (!istr__is_finished(in)) {
+ switch (*in->cur) {
+ case '\n': case '\r':
+ return;
+ case '#': case ';':
+ istr__ignore(in, '\n');
+ break;
+ default:
+ ini__add_value(table, in, options);
+ break;
+ }
+ }
+}
+
+static void ini__add_value(initable_t* table, ini__istream_t* in, const iniopts_t* options) {
+ if (!table) return;
+
+ inistrv_t key = strv__trim(istr__get_view(in, options->key_value_divider));
+ istr__skip(in); // skip divider
+ inistrv_t val = strv__trim(istr__get_view(in, '\n'));
+
+ if (strv__is_empty(key)) return;
+
+ // find inline comments
+ for (size_t i = 0; i < val.len; ++i) {
+ if (val.buf[i] == '#' || val.buf[i] == ';') {
+ // can escape # and ; with a backslash (e.g. \#)
+ if (i > 0 && val.buf[i - 1] == '\\') {
+ continue;
+ }
+ val.len = i;
+ break;
+ }
+ }
+
+ // value might be until EOF, in that case no use in skipping
+ if (!istr__is_finished(in)) istr__skip(in); // skip \n
+ inivalue_t* new_val = options->override_duplicate_keys ? ini__find_value(table, key) : NULL;
+ if (new_val) {
+ new_val->value = val;
+ }
+ else {
+ ivec_push(table->values, CDECL(inivalue_t){ key, val });
+ }
+}
+
+static char* ini__strdup(const char* src, size_t len) {
+ if (!src || len == 0) return NULL;
+ char* buf = (char*)malloc(len + 1);
+ if (!buf) return NULL;
+ memcpy(buf, src, len);
+ buf[len] = '\0';
+ return buf;
+}
+
+static int ini__rem_escaped(inistrv_t value, char* buf, size_t buflen) {
+ if (!buf || buflen == 0) return INI_INVALID_ARGS;
+ if (strv__is_empty(value)) {
+ buf[0] = '\0';
+ return 0;
+ }
+ size_t len = value.len - 1;
+ size_t dest_pos = 0;
+ const char* src = value.buf;
+
+ for (
+ size_t src_pos = 0;
+ src_pos < len;
+ ++src_pos, ++dest_pos
+ ) {
+ if (dest_pos >= buflen) return INI_BUFFER_TOO_SMALL;
+
+ if (src[src_pos] == '\\' &&
+ (src[src_pos + 1] == ';' || src[src_pos + 1] == '#')
+ ) {
+ src_pos++;
+ }
+
+ buf[dest_pos] = src[src_pos];
+ }
+
+ if (dest_pos >= buflen) return INI_BUFFER_TOO_SMALL;
+ buf[dest_pos++] = value.buf[len];
+ buf[dest_pos] = '\0';
+ return dest_pos;
+}
+
+static ini__istream_t istr__init(const char* str, size_t len) {
+ return CDECL(ini__istream_t) { str, str, len };
+}
+
+static bool istr__is_finished(ini__istream_t* in) {
+ return (size_t)(in->cur - in->start) >= in->len;
+}
+
+static void istr__skip_whitespace(ini__istream_t* in) {
+ const char* end = in->start + in->len;
+ while (in->cur < end && isspace(*in->cur)) {
+ ++in->cur;
+ }
+}
+
+static void istr__ignore(ini__istream_t* in, char delim) {
+ const char* end = in->start + in->len;
+ for (; in->cur < end && *in->cur != delim; ++in->cur);
+}
+
+static void istr__skip(ini__istream_t* in) {
+ if (!istr__is_finished(in)) ++in->cur;
+}
+
+static inistrv_t istr__get_view(ini__istream_t* in, char delim) {
+ const char* from = in->cur;
+ istr__ignore(in, delim);
+ size_t len = in->cur - from;
+ return CDECL(inistrv_t) { from, len };
+}
+
+static inistrv_t strv__from_str(const char* str) {
+ return CDECL(inistrv_t) { str, str ? strlen(str) : 0 };
+}
+
+static inistrv_t strv__trim(inistrv_t view) {
+ if (strv__is_empty(view)) return view;
+ inistrv_t out = view;
+ // trim left
+ for (size_t i = 0; i < view.len && isspace(view.buf[i]); ++i) {
+ ++out.buf;
+ --out.len;
+ }
+ if (strv__is_empty(out)) return view;
+ // trim right
+ for (long long i = view.len - 1; i >= 0 && isspace(view.buf[i]); --i) {
+ --out.len;
+ }
+ return out;
+}
+
+static inistrv_t strv__sub(inistrv_t view, size_t from, size_t to) {
+ if (to > view.len) to = view.len;
+ if (from > to) from = to;
+ return CDECL(inistrv_t) { view.buf + from, to - from };
+}
+
+static bool strv__is_empty(inistrv_t view) {
+ return view.len == 0;
+}
+
+static int strv__cmp(inistrv_t a, inistrv_t b) {
+ if (a.len < b.len) return -1;
+ if (a.len > b.len) return 1;
+ return memcmp(a.buf, b.buf, a.len);
+}
+
+#endif
+
+/*
+MIT LICENSE
+Copyright 2022 snarmph
+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.
+*/
\ No newline at end of file
diff --git a/infzoom/external/log.c b/infzoom/external/log.c
new file mode 100644
index 0000000..713cce0
--- /dev/null
+++ b/infzoom/external/log.c
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2020 rxi
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "log.h"
+
+#define MAX_CALLBACKS 32
+
+typedef struct {
+ log_LogFn fn;
+ void *udata;
+ int level;
+} Callback;
+
+static struct {
+ void *udata;
+ log_LockFn lock;
+ int level;
+ bool quiet;
+ Callback callbacks[MAX_CALLBACKS];
+} L;
+
+
+static const char *level_strings[] = {
+ "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"
+};
+
+#ifdef LOG_USE_COLOR
+static const char *level_colors[] = {
+ "\x1b[94m", "\x1b[36m", "\x1b[32m", "\x1b[33m", "\x1b[31m", "\x1b[35m"
+};
+#endif
+
+
+static void stdout_callback(log_Event *ev) {
+ // char buf[16];
+ // buf[strftime(buf, sizeof(buf), "%H:%M:%S", ev->time)] = '\0';
+#ifdef LOG_USE_COLOR
+ fprintf(
+ ev->udata, "%s%-5s\x1b[0m ",
+ level_colors[ev->level], level_strings[ev->level]);
+#else
+
+ // fprintf(
+ // ev->udata, "%s %-5s %s:%d: ",
+ // buf, level_strings[ev->level], ev->file, ev->line);
+
+ fprintf(
+ ev->udata, "[%-5s] ",
+ level_strings[ev->level]);
+
+#endif
+ vfprintf(ev->udata, ev->fmt, ev->ap);
+ fprintf(ev->udata, "\n");
+ fflush(ev->udata);
+}
+
+
+static void file_callback(log_Event *ev) {
+ char buf[64];
+ buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0';
+ fprintf(
+ ev->udata, "%s %-5s: ",
+ buf, level_strings[ev->level]);
+ vfprintf(ev->udata, ev->fmt, ev->ap);
+ fprintf(ev->udata, "\n");
+ fflush(ev->udata);
+}
+
+
+static void lock(void) {
+ if (L.lock) { L.lock(true, L.udata); }
+}
+
+
+static void unlock(void) {
+ if (L.lock) { L.lock(false, L.udata); }
+}
+
+
+const char* log_level_string(int level) {
+ return level_strings[level];
+}
+
+
+void log_set_lock(log_LockFn fn, void *udata) {
+ L.lock = fn;
+ L.udata = udata;
+}
+
+
+void log_set_level(int level) {
+ L.level = level;
+}
+
+
+void log_set_quiet(bool enable) {
+ L.quiet = enable;
+}
+
+
+int log_add_callback(log_LogFn fn, void *udata, int level) {
+ for (int i = 0; i < MAX_CALLBACKS; i++) {
+ if (!L.callbacks[i].fn) {
+ L.callbacks[i] = (Callback) { fn, udata, level };
+ return 0;
+ }
+ }
+ return -1;
+}
+
+
+int log_add_fp(FILE *fp, int level) {
+ return log_add_callback(file_callback, fp, level);
+}
+
+
+static void init_event(log_Event *ev, void *udata) {
+ if (!ev->time) {
+ time_t t = time(NULL);
+ ev->time = localtime(&t);
+ }
+ ev->udata = udata;
+}
+
+
+void log_log(int level, const char *file, int line, const char *fmt, ...) {
+ log_Event ev = {
+ .fmt = fmt,
+ .file = file,
+ .line = line,
+ .level = level,
+ };
+
+ lock();
+
+ if (!L.quiet && level >= L.level) {
+ init_event(&ev, stderr);
+ va_start(ev.ap, fmt);
+ stdout_callback(&ev);
+ va_end(ev.ap);
+ }
+
+ for (int i = 0; i < MAX_CALLBACKS && L.callbacks[i].fn; i++) {
+ Callback *cb = &L.callbacks[i];
+ if (level >= cb->level) {
+ init_event(&ev, cb->udata);
+ va_start(ev.ap, fmt);
+ cb->fn(&ev);
+ va_end(ev.ap);
+ }
+ }
+
+ unlock();
+}
diff --git a/infzoom/external/log.h b/infzoom/external/log.h
new file mode 100644
index 0000000..b1fae24
--- /dev/null
+++ b/infzoom/external/log.h
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2020 rxi
+ *
+ * This library is free software; you can redistribute it and/or modify it
+ * under the terms of the MIT license. See `log.c` for details.
+ */
+
+#ifndef LOG_H
+#define LOG_H
+
+#include
+#include
+#include
+#include
+
+#define LOG_VERSION "0.1.0"
+
+typedef struct {
+ va_list ap;
+ const char *fmt;
+ const char *file;
+ struct tm *time;
+ void *udata;
+ int line;
+ int level;
+} log_Event;
+
+typedef void (*log_LogFn)(log_Event *ev);
+typedef void (*log_LockFn)(bool lock, void *udata);
+
+enum { LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL };
+
+#define log_trace(...) log_log(LOG_TRACE, __FILE__, __LINE__, __VA_ARGS__)
+#define log_debug(...) log_log(LOG_DEBUG, __FILE__, __LINE__, __VA_ARGS__)
+#define log_info(...) log_log(LOG_INFO, __FILE__, __LINE__, __VA_ARGS__)
+#define log_warn(...) log_log(LOG_WARN, __FILE__, __LINE__, __VA_ARGS__)
+#define log_error(...) log_log(LOG_ERROR, __FILE__, __LINE__, __VA_ARGS__)
+#define log_fatal(...) log_log(LOG_FATAL, __FILE__, __LINE__, __VA_ARGS__)
+
+const char* log_level_string(int level);
+void log_set_lock(log_LockFn fn, void *udata);
+void log_set_level(int level);
+void log_set_quiet(bool enable);
+int log_add_callback(log_LogFn fn, void *udata, int level);
+int log_add_fp(FILE *fp, int level);
+
+void log_log(int level, const char *file, int line, const char *fmt, ...);
+
+#endif
diff --git a/infzoom/infzoom.c b/infzoom/infzoom.c
new file mode 100644
index 0000000..7662939
--- /dev/null
+++ b/infzoom/infzoom.c
@@ -0,0 +1,509 @@
+#include "infzoom.h"
+
+#define INI_IMPLEMENTATION
+#include
+
+typedef union _HOTKEY {
+ struct {
+ USHORT Mod;
+ USHORT Key;
+ } k;
+ UINT AsUInt;
+} HOTKEY, *PHOTKEY;
+
+typedef struct _RELATIVE_RECT {
+ SHORT OffsetX;
+ SHORT OffsetY;
+ USHORT Width;
+ USHORT Height;
+} RELATIVE_RECT, *PRELATIVE_RECT;
+
+typedef struct _CONFIG {
+ ULONG monitor;
+ HOTKEY HotKeyExit;
+ HOTKEY HotKeyNormal;
+ HOTKEY HotKey1P;
+ HOTKEY HotKey2P;
+ HOTKEY HotKeyDP;
+ HOTKEY HotKeyUp;
+ HOTKEY HotKeyDown;
+ HOTKEY HotKeyLeft;
+ HOTKEY HotKeyRight;
+ HOTKEY HotKeyLong;
+ HOTKEY HotKeyShort;
+ HOTKEY HotKeyNarrow;
+ HOTKEY HotKeyWide;
+ RELATIVE_RECT ZoomManual;
+ RELATIVE_RECT Zoom1P;
+ RELATIVE_RECT Zoom2P;
+ RELATIVE_RECT ZoomDP;
+} CONFIG, *PCONFIG;
+
+typedef struct _MONITOR_DATA {
+ ULONG CurrentCount;
+ ULONG TargetCount;
+ HMONITOR Monitor;
+ LONG OffsetX;
+ LONG OffsetY;
+ LONG Width;
+ LONG Height;
+} MONITOR_DATA, *PMONITOR_DATA;
+
+CONFIG GlobalConfig;
+int HotKeyId;
+MONITOR_DATA GlobalMonitorData;
+HWND InfWindow;
+FILE *fp;
+
+INT
+CleanupBeforeExit(
+ INT ExitCode
+ )
+{
+ if (fp != NULL) {
+ log_info("*** InfZoom.exe is exiting *** ");
+ fclose(fp);
+ fp = NULL;
+ }
+ ExitProcess(ExitCode);
+ return ExitCode;
+}
+
+DECLSPEC_NORETURN
+VOID
+FailFast (
+ _In_ UINT ExitCode
+ )
+{
+ log_fatal("FATAL: Exiting with error 0x%x", ExitCode);
+ CleanupBeforeExit(ExitCode);
+}
+
+VOID
+ParseConfigForHotkey(
+ initable_t* IniRoot,
+ PCHAR Name,
+ PHOTKEY Hotkey
+ )
+{
+ CHAR Buffer[24];
+
+ sprintf(Buffer, "%s_mod", Name);
+ Hotkey->k.Mod = (USHORT)ini_as_uint(ini_get(IniRoot, Buffer));
+ sprintf(Buffer, "%s_key", Name);
+ Hotkey->k.Key = (USHORT)ini_as_uint(ini_get(IniRoot, Buffer));
+ log_trace("CONFIG: hotkey %s: 0x%x + 0x%x", Name, Hotkey->k.Mod, Hotkey->k.Key);
+}
+
+VOID
+ParseConfigForZoom(
+ initable_t* IniRoot,
+ PCHAR Name,
+ PRELATIVE_RECT RelRect
+ )
+{
+ CHAR Buffer[24];
+
+ sprintf(Buffer, "%s_zoom_x", Name);
+ RelRect->OffsetX = (USHORT)ini_as_uint(ini_get(IniRoot, Buffer));
+ sprintf(Buffer, "%s_zoom_y", Name);
+ RelRect->OffsetY = (USHORT)ini_as_uint(ini_get(IniRoot, Buffer));
+ sprintf(Buffer, "%s_zoom_w", Name);
+ RelRect->Width = (USHORT)ini_as_uint(ini_get(IniRoot, Buffer));
+ sprintf(Buffer, "%s_zoom_h", Name);
+ RelRect->Height = (USHORT)ini_as_uint(ini_get(IniRoot, Buffer));
+ log_trace(
+ "CONFIG: zoom %s: %d %d %d %d",
+ Name,
+ RelRect->OffsetX,
+ RelRect->OffsetY,
+ RelRect->Width,
+ RelRect->Height);
+}
+
+
+VOID
+ParseConfig(
+ PCONFIG Config
+ )
+
+{
+ ini_t ini = ini_parse("infzoom.ini", NULL);
+ initable_t* root = ini_get_table(&ini, INI_ROOT);
+
+ Config->monitor = (ULONG)ini_as_uint(ini_get(root, "monitor"));
+ log_trace("CONFIG: monitor %d", Config->monitor);
+
+ ParseConfigForHotkey(root, "exit", &GlobalConfig.HotKeyExit);
+ ParseConfigForHotkey(root, "normal", &GlobalConfig.HotKeyNormal);
+
+ ParseConfigForHotkey(root, "1p", &GlobalConfig.HotKey1P);
+ ParseConfigForHotkey(root, "2p", &GlobalConfig.HotKey2P);
+ ParseConfigForHotkey(root, "dp", &GlobalConfig.HotKeyDP);
+
+ ParseConfigForHotkey(root, "up", &GlobalConfig.HotKeyUp);
+ ParseConfigForHotkey(root, "down", &GlobalConfig.HotKeyDown);
+ ParseConfigForHotkey(root, "left", &GlobalConfig.HotKeyLeft);
+ ParseConfigForHotkey(root, "right", &GlobalConfig.HotKeyRight);
+
+ ParseConfigForHotkey(root, "long", &GlobalConfig.HotKeyLong);
+ ParseConfigForHotkey(root, "short", &GlobalConfig.HotKeyShort);
+ ParseConfigForHotkey(root, "narrow", &GlobalConfig.HotKeyNarrow);
+ ParseConfigForHotkey(root, "wide", &GlobalConfig.HotKeyWide);
+
+ ParseConfigForZoom(root, "1p", &GlobalConfig.Zoom1P);
+ ParseConfigForZoom(root, "2p", &GlobalConfig.Zoom2P);
+ ParseConfigForZoom(root, "dp", &GlobalConfig.ZoomDP);
+
+ ini_free(&ini);
+}
+
+HMONITOR
+GetPrimaryMonitorHandle(
+ VOID
+ )
+{
+ const POINT ptZero = { 0, 0 };
+ return MonitorFromPoint(ptZero, MONITOR_DEFAULTTOPRIMARY);
+}
+
+VOID
+FillMonitorData(
+ HMONITOR Monitor,
+ PMONITOR_DATA MonitorData
+ )
+{
+ MONITORINFO Info;
+ Info.cbSize = sizeof(Info);
+ if (GetMonitorInfo(Monitor, &Info)) {
+ MonitorData->Monitor = Monitor;
+ MonitorData->Width = abs(Info.rcMonitor.right - Info.rcMonitor.left);
+ MonitorData->Height = abs(Info.rcMonitor.top - Info.rcMonitor.bottom);
+ MonitorData->OffsetX = Info.rcMonitor.left;
+ MonitorData->OffsetY = Info.rcMonitor.top;
+ }
+}
+
+BOOL
+CALLBACK
+MonitorEnumProc(
+ HMONITOR hMonitor,
+ HDC hdcMonitor,
+ LPRECT lprcMonitor,
+ LPARAM dwData)
+{
+ PMONITOR_DATA MonitorData;
+
+ MonitorData = (PMONITOR_DATA )dwData;
+ MonitorData->CurrentCount += 1;
+ if (MonitorData->CurrentCount != MonitorData->TargetCount) {
+ // return TRUE to continue enumerating
+ return TRUE;
+ }
+
+ FillMonitorData(hMonitor, MonitorData);
+ // return FALSE to stop enumerating
+ return FALSE;
+}
+
+VOID
+ResizeWindow (
+ int x,
+ int y,
+ int w,
+ int h
+ )
+{
+ BOOL BoolResult;
+
+ // SetWindowPos works better than MoveWindow in this case, because of SWP_NOSENDCHANGING.
+ // SWP_NOSENDCHANGING allows us to bypass the checks done by the game which restricts how big
+ // the window can be (usually capped around 120% or so).
+
+#if TRUE
+
+ DWORD Flags;
+
+ Flags = (
+ SWP_ASYNCWINDOWPOS |
+ SWP_NOCOPYBITS |
+ SWP_NOREDRAW |
+ SWP_NOSENDCHANGING
+ );
+
+ BoolResult = SetWindowPos(
+ InfWindow,
+ HWND_TOP,
+ x,
+ y,
+ w,
+ h,
+ Flags);
+
+ if (!BoolResult) {
+ log_error("ERROR: Call to SetWindowPos failed: GLE: %d", GetLastError());
+ }
+
+#endif
+
+#if FALSE
+
+ BoolResult = MoveWindow(
+ InfWindow,
+ x,
+ y,
+ w,
+ h,
+ true); // repaint
+
+ if (!BoolResult) {
+ log_error("ERROR: Call to MoveWindow failed: GLE: %d", GetLastError());
+ }
+
+#endif
+
+ return;
+}
+
+VOID
+GetMonitorsHandle(
+ ULONG monitor,
+ PMONITOR_DATA MonitorData
+ )
+{
+ ZeroMemory(MonitorData, sizeof(*MonitorData));
+
+ // fill with primary monitor info first
+ FillMonitorData(GetPrimaryMonitorHandle(), MonitorData);
+ MonitorData->TargetCount = monitor;
+
+ // and then enumerate as needed
+ if (monitor != 0) {
+ EnumDisplayMonitors(NULL, NULL, MonitorEnumProc, (LPARAM)MonitorData);
+ }
+}
+
+VOID
+MoveWindowRelative(
+ PMONITOR_DATA MonitorData,
+ PRELATIVE_RECT RelRect
+ )
+{
+ int x, y, w, h;
+
+ // start with borderless fullscreen to fill the monitor
+ x = MonitorData->OffsetX;
+ y = MonitorData->OffsetY;
+ w = MonitorData->Width;
+ h = MonitorData->Height;
+
+ // apply resizes first
+ if (RelRect->Width != 100) {
+ w = (MonitorData->Width * RelRect->Width) / 1000;
+ }
+ if (RelRect->Height != 100) {
+ h = (MonitorData->Height * RelRect->Height) / 1000;
+ }
+
+ // center to the screen
+ x += (MonitorData->Width - w) / 2;
+ y += (MonitorData->Height - h) / 2;
+
+ // apply offsets
+ if (RelRect->OffsetX != 0) {
+ x += (MonitorData->Width * RelRect->OffsetX) / 1000;
+ }
+ if (RelRect->OffsetY != 0) {
+ y += (MonitorData->Height * RelRect->OffsetY) / 1000;
+ }
+
+ log_trace(
+ " x:%d (%.1f%%) y:%d (%.1f%%) w:%d (%.1f%%) h:%d (%.1f%%)",
+ x, RelRect->OffsetX / 10.0,
+ y, RelRect->OffsetY / 10.0,
+ w, RelRect->Width / 10.0,
+ h, RelRect->Height / 10.0);
+
+ ResizeWindow(x, y, w, h);
+}
+
+BOOL AllowManualMove = FALSE;
+
+VOID
+ResetManualZoom (
+ VOID
+ )
+{
+ AllowManualMove = TRUE;
+ GlobalConfig.ZoomManual.OffsetX = 0;
+ GlobalConfig.ZoomManual.OffsetY = 0;
+ GlobalConfig.ZoomManual.Width = 1000;
+ GlobalConfig.ZoomManual.Height = 1000;
+}
+
+USHORT ActiveModifiers = 0;
+
+BOOL
+TestHotKeyDown (
+ PRAWKEYBOARD Kbd,
+ PHOTKEY Hotkey
+ )
+{
+ if (Hotkey->k.Mod != 0) {
+ if ((ActiveModifiers & Hotkey->k.Mod) == 0) {
+ return FALSE;
+ }
+ }
+ return (Hotkey->k.Key == Kbd->VKey);
+}
+
+VOID
+MessageLoop(
+ PRAWKEYBOARD Kbd
+ )
+{
+ USHORT ModifierChange;
+
+ // keep track of modifiers
+ ModifierChange = 0;
+ switch (Kbd->VKey) {
+ case VK_SHIFT:
+ ModifierChange = MOD_SHIFT;
+ break;
+ case VK_MENU:
+ ModifierChange = MOD_ALT;
+ break;
+ case VK_CONTROL:
+ ModifierChange = MOD_CONTROL;
+ break;
+ }
+ if (Kbd->Message == WM_KEYDOWN || Kbd->Message == WM_SYSKEYDOWN) {
+ ActiveModifiers |= ModifierChange;
+ } else if (Kbd->Message == WM_KEYUP || Kbd->Message == WM_SYSKEYUP) {
+ ActiveModifiers &= ~ModifierChange;
+ }
+
+ if (Kbd->Message != WM_KEYDOWN && Kbd->Message != WM_SYSKEYDOWN) {
+ return;
+ }
+
+ // everything below tests for WM_KEYDOWN / WM_SYSKEYDOWN events.
+
+ if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyExit)) {
+ log_info("HOTKEY: exit game...");
+ PostMessage(InfWindow, WM_CLOSE, 0, 0);
+ CleanupBeforeExit(0);
+
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyNormal)) {
+ log_info("HOTKEY: reset zoom to normal view (manual adjustments possible)... ");
+ ResetManualZoom();
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKey1P)) {
+ log_info("HOTKEY: activate 1P zoom view... ");
+ AllowManualMove = FALSE;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.Zoom1P);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKey2P)) {
+ log_info("HOTKEY: activate 2P zoom view... ");
+ AllowManualMove = FALSE;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.Zoom2P);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyDP)) {
+ log_info("HOTKEY: activate DP zoom view... ");
+ AllowManualMove = FALSE;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomDP);
+
+ } else if (AllowManualMove) {
+ if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyUp)) {
+ log_info("HOTKEY: manual move UP... ");
+ GlobalConfig.ZoomManual.OffsetY -= 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyDown)) {
+ log_info("HOTKEY: manual move DOWN... ");
+ GlobalConfig.ZoomManual.OffsetY += 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyLeft)) {
+ log_info("HOTKEY: manual move LEFT... ");
+ GlobalConfig.ZoomManual.OffsetX -= 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyRight)) {
+ log_info("HOTKEY: manual move RIGHT... ");
+ GlobalConfig.ZoomManual.OffsetX += 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyLong)) {
+ log_info("HOTKEY: manual increase height... ");
+ GlobalConfig.ZoomManual.Height += 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyShort)) {
+ log_info("HOTKEY: manual reduce height... ");
+ GlobalConfig.ZoomManual.Height -= 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyWide)) {
+ log_info("HOTKEY: manual increase width... ");
+ GlobalConfig.ZoomManual.Width += 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ } else if (TestHotKeyDown(Kbd, &GlobalConfig.HotKeyNarrow)) {
+ log_info("HOTKEY: manual reduce width... ");
+ GlobalConfig.ZoomManual.Width -= 1;
+ MoveWindowRelative(&GlobalMonitorData, &GlobalConfig.ZoomManual);
+ }
+ }
+}
+
+int
+main(
+ int argc,
+ char* argv[]
+ )
+{
+ LONG Result;
+
+ fp = fopen("infzoom.log", "w");
+ if (fp == NULL) {
+ printf("Couldn't open infzoom.log for write");
+ return CleanupBeforeExit(-1);
+ }
+
+ log_add_fp(fp, LOG_TRACE);
+
+ log_info("*** InfZoom.exe *** ");
+
+ // parse config.json
+ log_info("Config parsing start...");
+ ParseConfig(&GlobalConfig);
+
+ // find monitor
+ GetMonitorsHandle(GlobalConfig.monitor, &GlobalMonitorData);
+ ResetManualZoom();
+
+ // find window handle from args
+ if (argc < 2) {
+ log_fatal("ERROR: Missing handle to window");
+ return CleanupBeforeExit(-1);
+ }
+ InfWindow = (HWND)atoi(argv[1]);
+ log_info("Using window handle 0x%p", InfWindow);
+
+ // make window borderless
+ log_info("Call SetWindowLong to make window borderless...");
+ Result = SetWindowLong(
+ InfWindow,
+ GWL_STYLE,
+ WS_VISIBLE | WS_CLIPSIBLINGS);
+
+ if (Result == 0) {
+ log_error("ERROR: SetWindowLong failed: GLE: %d", GetLastError());
+ }
+
+ // do initial call to resize
+ log_info("Call ResizeWindow to make window fit the screen...");
+ ResizeWindow(
+ GlobalMonitorData.OffsetX,
+ GlobalMonitorData.OffsetY,
+ GlobalMonitorData.Width,
+ GlobalMonitorData.Height);
+
+ log_info("Create an invisible window to capture hotkeys...");
+ CreateNewWindow();
+
+ return CleanupBeforeExit(0);
+}
diff --git a/infzoom/infzoom.h b/infzoom/infzoom.h
new file mode 100644
index 0000000..eb32b98
--- /dev/null
+++ b/infzoom/infzoom.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+DECLSPEC_NORETURN
+VOID
+FailFast (
+ _In_ UINT ExitCode
+ );
+
+VOID
+MessageLoop(
+ PRAWKEYBOARD Kbd
+ );
+
+// window.c
+VOID
+CreateNewWindow(
+ VOID
+ );
+
+VOID
+RegisterRawInput(
+ VOID
+ );
diff --git a/infzoom/infzoom.ini b/infzoom/infzoom.ini
new file mode 100644
index 0000000..cee5870
--- /dev/null
+++ b/infzoom/infzoom.ini
@@ -0,0 +1,105 @@
+# config file for infzoom.exe
+
+#
+# General
+#
+
+# Which monitor should be used for fullscreen borderless?
+# 0 = primary monitor
+# 1 = monitor #1, 2 = monitor #2...
+
+monitor = 0
+
+#
+# Zoom
+#
+
+# all units are relative to full screen (out of 1000)
+# abc_zoom_x = x-offset (can be negative)
+# abc_zoom_y = y-offset (can be negative)
+# abc_zoom_w = width (0 and up)
+# abc_zoom_h = height (0 and up)
+#
+# for example,
+# 1p_zoom_x = -339 = shift 33.9% left
+# 1p_zoom_y = 224 = shift 22.4% down
+# 1p_zoom_w = 1750 = set width to 175.0%
+# 1p_zoom_h = 846 = set height to 84.6%
+#
+# window is always **centered** to the screen, and then the offset is applied
+#
+# to figure out the right number, press F5 to put into manual mode, and use
+# arrow key (ctrl+arrow or alt+arrow) to manually move the window, take note of
+# the percentage values in the command line or the log file, and update the
+# values below
+#
+
+1p_zoom_x = 339
+1p_zoom_y = 12
+1p_zoom_w = 1750
+1p_zoom_h = 1480
+
+2p_zoom_x = -339
+2p_zoom_y = 12
+2p_zoom_w = 1750
+2p_zoom_h = 1480
+
+dp_zoom_x = 0
+dp_zoom_y = 134
+dp_zoom_w = 1800
+dp_zoom_h = 1640
+
+#
+# Hotkeys
+#
+
+# xyz_mod:
+# NONE : 0
+# ALT : 1
+# CTRL : 2
+# SHIFT: 4
+# WIN : 8
+# see https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
+
+# xyz_key:
+# see https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
+
+# exit: close infinitas. Default: ALT+F10
+exit_mod = 1
+exit_key = 0x79
+
+# 1p: zoom into 1p view. Default: F1
+1p_mod = 0
+1p_key = 0x70
+
+# 2p: zoom into 1p view. Default: F2
+2p_mod = 0
+2p_key = 0x71
+
+# dp: zoom into dp view. Default: F3
+dp_mod = 0
+dp_key = 0x72
+
+# normal: reset zoom to normal view. Default: F5
+normal_mod = 0
+normal_key = 0x74
+
+# manual moves: ctrl + arrow
+up_mod = 2
+up_key = 0x26
+down_mod = 2
+down_key = 0x28
+left_mod = 2
+left_key = 0x25
+right_mod = 2
+right_key = 0x27
+
+# manual zooms: alt + arrow
+long_mod = 1
+long_key = 0x26
+short_mod = 1
+short_key = 0x28
+narrow_mod = 1
+narrow_key = 0x25
+wide_mod = 1
+wide_key = 0x27
diff --git a/infzoom/infzoom.sln b/infzoom/infzoom.sln
new file mode 100644
index 0000000..4a84743
--- /dev/null
+++ b/infzoom/infzoom.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.32414.318
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "infzoom", "infzoom.vcxproj", "{BF3C8448-6170-43FB-824D-EC7F96334D9D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x86 = Debug|x86
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Debug|x86.ActiveCfg = Debug|Win32
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Debug|x86.Build.0 = Debug|Win32
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Release|x86.ActiveCfg = Release|Win32
+ {BF3C8448-6170-43FB-824D-EC7F96334D9D}.Release|x86.Build.0 = Release|Win32
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {AEC958AA-F6DA-4E1A-82B3-4CD01D281180}
+ EndGlobalSection
+EndGlobal
diff --git a/infzoom/infzoom.vcxproj b/infzoom/infzoom.vcxproj
new file mode 100644
index 0000000..eab244d
--- /dev/null
+++ b/infzoom/infzoom.vcxproj
@@ -0,0 +1,168 @@
+
+
+
+
+ Debug
+ Win32
+
+
+ Release
+ Win32
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ Win32Proj
+ {bf3c8448-6170-43fb-824d-ec7f96334d9d}
+ infzoom
+ 10.0.22000.0
+ infzoom
+
+
+
+ Application
+ true
+ v143
+ Unicode
+ true
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+ Application
+ true
+ v143
+ Unicode
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+
+ Level3
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+ stdcpp20
+ stdc17
+ $(ProjectDir)\external%(AdditionalIncludeDirectories)
+ MultiThreadedDebug
+ true
+
+
+ Console
+ true
+ %(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ true
+ true
+ _CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+ stdcpp20
+ stdc17
+ MultiThreaded
+ true
+ $(ProjectDir)\external%(AdditionalIncludeDirectories)
+
+
+ Console
+ true
+ true
+ true
+ %(AdditionalDependencies)
+
+
+
+
+ Level3
+ true
+ _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ true
+
+
+ Console
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/infzoom/infzoom.vcxproj.filters b/infzoom/infzoom.vcxproj.filters
new file mode 100644
index 0000000..356830f
--- /dev/null
+++ b/infzoom/infzoom.vcxproj.filters
@@ -0,0 +1,39 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
\ No newline at end of file
diff --git a/infzoom/infzoom.vcxproj.user b/infzoom/infzoom.vcxproj.user
new file mode 100644
index 0000000..37487f3
--- /dev/null
+++ b/infzoom/infzoom.vcxproj.user
@@ -0,0 +1,13 @@
+
+
+
+ 0
+ WindowsLocalDebugger
+ $(ProjectDir)\Debug
+
+
+ 0
+ WindowsLocalDebugger
+ $(ProjectDir)\Release
+
+
\ No newline at end of file
diff --git a/infzoom/window.c b/infzoom/window.c
new file mode 100644
index 0000000..b2b74a2
--- /dev/null
+++ b/infzoom/window.c
@@ -0,0 +1,156 @@
+#include "infzoom.h"
+
+const wchar_t CLASS_NAME[] = L"infzoom hotkey capture window";
+HWND hwnd;
+
+// pre-allocated for perf
+PRAWINPUT RawInputData;
+SIZE_T RawInputDataSize;
+
+VOID
+ProcessWmInput (
+ _In_ PRAWINPUT Data
+ )
+{
+ PRAWKEYBOARD Kbd;
+
+ if (Data->header.dwType != RIM_TYPEKEYBOARD) {
+ return;
+ }
+
+ Kbd = &Data->data.keyboard;
+ MessageLoop(Kbd);
+}
+
+LRESULT
+CALLBACK
+WndProc(
+ HWND hWnd,
+ UINT msg,
+ WPARAM wparam,
+ LPARAM lParam
+ )
+{
+ switch (msg) {
+ case WM_CREATE: {
+ LPCREATESTRUCT create_params = (LPCREATESTRUCT)lParam;
+ SetWindowLongPtr(
+ hWnd,
+ GWLP_USERDATA,
+ (LONG_PTR)(create_params->lpCreateParams));
+
+ return 0;
+ }
+ case WM_INPUT: {
+ UINT Size = RawInputDataSize;
+ UINT BytesReturned;
+ BytesReturned = GetRawInputData(
+ (HRAWINPUT)lParam,
+ RID_INPUT,
+ RawInputData,
+ &Size,
+ sizeof(RAWINPUTHEADER));
+ if (BytesReturned == -1) {
+ printf("ERROR: GetRawInputData failed, GLE = 0x%x\n", GetLastError());
+ break;
+ }
+
+ ProcessWmInput(RawInputData);
+ return 0;
+ }
+ default:
+ break;
+ }
+
+ return DefWindowProc(hWnd, msg, wparam, lParam);
+}
+
+DWORD
+WINAPI
+WindowThread(
+ LPVOID lpParam
+ )
+{
+ MSG msg;
+ PWNDCLASSEX wc;
+
+ wc = (PWNDCLASSEX)lpParam;
+
+ SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
+
+ // Create the window.
+ hwnd = CreateWindowEx(
+ WS_EX_NOACTIVATE,
+ CLASS_NAME,
+ L"infzoom window",
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ NULL,
+ NULL,
+ wc->hInstance,
+ NULL
+ );
+
+ if (hwnd == NULL) {
+ printf("ERROR: CreateWindowEx failed, GLE = 0x%x\n", GetLastError());
+ return 0;
+ }
+
+ RawInputDataSize = sizeof(*RawInputData) + 0x10000;
+ RawInputData = malloc(RawInputDataSize);
+ RegisterRawInput();
+
+ while (0 < GetMessage(&msg, hwnd, 0, 0)) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+
+ DestroyWindow(hwnd);
+ hwnd = NULL;
+ return 0;
+}
+
+VOID
+CreateNewWindow(
+ VOID
+ )
+{
+ HANDLE Thread;
+ WNDCLASSEX wc = {0};
+
+ wc.cbSize = sizeof(wc);
+ wc.lpfnWndProc = WndProc;
+ wc.hInstance = GetModuleHandle(NULL);
+ wc.lpszClassName = CLASS_NAME;
+ if (!RegisterClassEx(&wc)) {
+ FailFast(GetLastError());
+ }
+
+ Thread = CreateThread(NULL, 0, WindowThread, &wc, 0, NULL);
+ if (Thread == NULL) {
+ FailFast(GetLastError());
+ }
+ WaitForSingleObject(Thread, INFINITE);
+
+ UnregisterClass(CLASS_NAME, wc.hInstance);
+}
+
+VOID
+RegisterRawInput(
+ VOID
+ )
+{
+ RAWINPUTDEVICE device = {0};
+ device.hwndTarget = hwnd;
+
+ // keyboard
+ device.dwFlags = RIDEV_INPUTSINK;
+ device.usUsagePage = 1;
+ device.usUsage = 0x06;
+ if (!RegisterRawInputDevices(&device, 1, sizeof(device))) {
+ printf("RegisterRawInputDevices failed, GLE = 0x%x\n", GetLastError());
+ }
+}
\ No newline at end of file
diff --git a/package.bat b/package.bat
new file mode 100644
index 0000000..967a172
--- /dev/null
+++ b/package.bat
@@ -0,0 +1,19 @@
+for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /format:list') do set datetime=%%I
+set datetime=%datetime:~0,8%-%datetime:~8,6%
+
+cd /d %~dp0
+del /q /s dist\infzoom
+mkdir dist\infzoom
+
+copy /y LICENSE dist\infzoom\LICENSE.txt
+copy /y README.md dist\infzoom
+
+copy /y inf_launch_ext.ps1 dist\infzoom
+copy /y inf_launch_ext.ps1 dist\infzoom\inf_launch_ext_kr.ps1
+
+copy /y infzoom\infzoom.ini dist\infzoom
+copy /y infzoom\Release\infzoom.exe dist\infzoom
+
+pushd dist
+7z a -tzip -r infzoom-%datetime%.zip infzoom
+popd