This manual patch applier was originally made to apply ISXPM (Inno Setup XDELTA Patch Maker) game patches. It is extremely useful in a scenario when ISXPM patch mismatches in one or several resulting files and then rolls back whole patch.
The default ISXPM behavior is
- Unpack xdeltas in PatchData folder into temporary subfolder in %TEMP% (if patch was made with "Place patch-data inside a patch" option)
- Apply all xdeltas and save all original unpatched files in Backup folder. If many big files were patched Backup folder will require lots of space to accomodate all of them!
- If there was some error or hash mismatch then roll back the patch - restore all unpatched files from Backup folder
My manual patcher was made to
- Remove the need for big Backup folder. If a file is successfuly patched (no error was reported by single-file patcher) then unpatched file can be immediately replaced with newer patched one (OVERWRITE_SOURCE_FILES variable)
- Not to roll back whole patch when all of the files except one or two were successfully patched and these mismatched files can be selectively obtained from some other source e.g. torrent download
How to use this for any ISXPM patch
- Create ISXPM_MANUAL.bat (copy and paste the code below)
- Obtain PatchData folder (set patchdir variable accordingly if it is not named as PatchData by default). This folder is separate in patches created without "Place patch-data inside a patch" option. If it is not separate then it is packed inside the patch .exe/.bin and you need to do a trial install of the patch into some empty folder so that PatchData is unpacked into %TEMP% during patching and then you obtain it from %TEMP%.
- Identify xdelta file extension (patchext variable). Some files in PatchData e.g. those that are newly added are not xdeltas!
- Identify single-file patcher that is needed to apply xdeltas (patcher_name variable). ISXPM supports Xdelta, JojoDiff, HDiffPatch. Put the executable into PatchData or beside the .bat.
- Set patcher_params variable for specific single-file patcher if needed
This manual patch applier also supports the following ISXPM features, read the source for usage:
- PatchData on another drive. This feature is essential for small SSD which should only need space for biggest resulting file in the patch to successfully apply whole patch
- explicit _patch.lst (patch_lst_name variable) beside the .bat to apply only a subset of xdeltas
- key file used to check game version
- list of junked files or folders
- prepatch script used to rename/rearrange files before patching
- ExternalData subfolder with files in .arc archive. These are new files not covered by xdeltas in PatchData and compressed separately
ISXPM_MANUAL.bat
@echo off
rem ISXPM_MANUAL (c) 2026 Anton Shabunin https://ant-sh.blogspot.com
rem Applies xdeltas from ISXPM PatchData folder
set IMDEBUG=0
rem PatchData is default folder name in ISXPM
set patchdir=PatchData
rem Extension of xdeltas must be unique e.g. not be used by any files in patched set so that xdeltas could be identified
set patchext=._hdif_
set patcher_name=hpatchz-x64.exe
rem multi-thread Parallel mode -p-5 now only support single compressed diffData(created by hdiffz -SD-stepSize)
set patcher_params=-p-5 -f
rem for xdelta3 set patcher_params=-d -s
set arcexe_name=Arc.exe
set outdir=_new
rem if OVERWRITE_SOURCE_FILES set to 0 the set of patched files will be in %outdir% folder e.g.
rem it will require at least the same space as taken by set of source files
set OVERWRITE_SOURCE_FILES=1
rem keyfile is a relative filename without "..."
rem If _keyfile.txt exists in %patchdir% or script folder it will override static keyfile and keyfilehash vars below
set keyfile=
set keyfilehash=
rem keyfilealgo will be set dynamically based on the length of keyfilehash
rem 32 byte hash is MD5
rem 40 byte hash is SHA1
rem certutil hash algorithms: MD2 MD4 MD5 SHA1 SHA256 SHA384 SHA512
rem if file in _patch folder (patchdir variable) is not an xdelta but
rem instead a prepatched file without %patchext%
rem then it will be moved to _new (outdir variable)
rem or directly to destination if OVERWRITE_SOURCE_FILES set to 1
set notpatched_list=_patch.bad
set patch_lst_name=_patch.lst
set keyfile_txt_name=_keyfile.txt
set junked_lst_name=_junked.lst
set prepatch_cmd_name=_prepatch.cmd
rem Custom patchdir support
set DELETE_FILES_FROM_PATCHDIR=1
rem Default is to delete files from local patchdir
rem but not touch custom patchdir specified on command line
rem as it may be an archival copy stored on another drive.
rem If you want to move files from custom patchdir and delete it then
rem comment out or remove line set DELETE_FILES_FROM_PATCHDIR=0 below
set dirtype=default
if not "%~1"=="" (
set DELETE_FILES_FROM_PATCHDIR=0
set patchdir=%~1
set dirtype=custom
)
if not exist "%patchdir%" (
echo Patch data %dirtype% subfolder "%patchdir%" not found
echo Put it into game folder beside %~nx0 ^(it will be deleted^)
echo OR
echo specify path to it as a command line parameter to %~nx0
goto :exiting
) else (
echo Using %dirtype% patch data subfolder "%patchdir%" - DELETE_FILES_FROM_PATCHDIR is %DELETE_FILES_FROM_PATCHDIR%
)
rem ================ Here %patchdir% is good =========================
setlocal enabledelayedexpansion
rem Patcher exe must be suitable for xdeltas in %patchdir%
if exist "%patchdir%\%patcher_name%" set patcher_exe="%patchdir%\%patcher_name%"
if exist "%patcher_name%" set patcher_exe="%patcher_name%"
if not defined patcher_exe (
echo ERROR Single-file patcher executable %patcher_name% not found
echo Put it into "%patchdir%" or into game folder beside %~nx0
goto :exiting
)
rem Before doing anything check keyfile if it is specified
if exist "%patchdir%\%keyfile_txt_name%" set keyfile_txt="%patchdir%\%keyfile_txt_name%"
if exist "%keyfile_txt_name%" set keyfile_txt="%keyfile_txt_name%"
if defined keyfile_txt (
rem without usebackq filename with spaces inside "... " is interpreted as literal string and not file name
for /f "usebackq tokens=1,*" %%A in (%keyfile_txt%) do (
set keyfile=%%B
set keyfilehash=%%A
rem check if filename extracted from _keyfile.txt starts with * then remove it
set firstchar=!keyfile:~0,1!
if {!firstchar!}=={^*} (
if {%IMDEBUG%}=={1} echo removing leading ^* from keyfile name
set keyfile=!keyfile:~1!
)
)
)
if {%IMDEBUG%}=={1} (
echo ============================ DEBUG IS ENABLED ===========================
echo keyfile_txt is %keyfile_txt%
echo keyfile is %keyfile%, expected keyfilehash %keyfilehash%
)
if exist %notpatched_list% (
echo %notpatched_list% exists - renaming it as %patch_lst_name% and disabling keyfile check.
echo It is at least 2nd run so keyfile might have already been patched
rem _patch.lst (patch_lst_name) in game folder takes precedence
rem and will disable dynamic _patch.lst generation from %patchdir%
move /y %notpatched_list% %patch_lst_name%
set keyfile=
)
rem Actually checking hash of keyfile
if not "%keyfile%"=="" (
if not exist "%keyfile%" (
echo Warning - keyfile "%keyfile%" specified but not found! - exiting
goto :exiting
) else (
if not {%keyfilehash%}=={} (
rem Determine keyfilealgo
call :stringlength %keyfilehash% keyfilehashlen
if !keyfilehashlen!==40 (
set keyfilealgo=SHA1
) else (
if !keyfilehashlen!==32 set keyfilealgo=MD5
)
if defined keyfilealgo (
for /f %%h in ('CertUtil -hashfile "%keyfile%" !keyfilealgo! ^| findstr "^[0-9a-z][0-9a-z]*$"') do (
echo Keyfile %keyfile% actual !keyfilealgo! hash is %%h
if /i {%keyfilehash%}=={%%h} (
echo Keyfile hash match OK - continuing
) else (
echo ERROR Keyfile specified but hash mismatches expected %keyfilehash% - exiting
goto :exiting
)
)
) else (
echo ERROR keyfilehash %keyfilehash% with length !keyfilehashlen! is not supported - exiting
goto :exiting
)
) else (
echo Keyfile hash not specified - continuing
)
)
) else (
echo Keyfile not specified - continuing
)
rem counting extension length
call :stringlength %patchext% length
echo Xdelta extension length is: %length%
rem ---------------------- prepatch prepare section begin ----------------------
rem Put commands to rearrange/rename/back up files before patching into _prepatch.cmd
rem if exist "test.txt" (
rem move test.txt test_prepatched.txt
rem )
if exist "%patchdir%\%prepatch_cmd_name%" set prepatch_cmd="%patchdir%\%prepatch_cmd_name%"
if exist "%prepatch_cmd_name%" set prepatch_cmd="%prepatch_cmd_name%"
echo prepatch_cmd is %prepatch_cmd%
if defined prepatch_cmd (
call %prepatch_cmd%
if not {%IMDEBUG%}=={1} (
if exist "%prepatch_cmd_name%" del /q "%prepatch_cmd_name%"
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} del /q %prepatch_cmd%
)
)
rem ---------------------- prepatch prepare section end ------------------------
if exist "%patchdir%\%junked_lst_name%" set junked_lst="%patchdir%\%junked_lst_name%"
if exist "%junked_lst_name%" set junked_lst="%junked_lst_name%"
echo junked_lst is %junked_lst%
if defined junked_lst (
for /f "usebackq tokens=*" %%f in (%junked_lst%) do (
if exist "%%~f" (
if not exist "%%~f\*" (
if {%IMDEBUG%}=={1} echo Deleting file "%%~f"
del /Q /F "%%~f"
) else (
if {%IMDEBUG%}=={1} echo Deleting folder "%%~f"
rd /s /q "%%~f"
)
)
)
if not {%IMDEBUG%}=={1} (
if exist "%junked_lst_name%" del /q "%junked_lst_name%"
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} del /q %junked_lst%
)
)
rem If no explicit _patch.lst in game folder must dynamically generate it from patchdir
if exist "%patch_lst_name%" (
set patch_lst="%patch_lst_name%"
goto :patching
)
set patch_lst="%TEMP%\%patch_lst_name%"
call :gen_patch_lst > %patch_lst%
rem if {%IMDEBUG%}=={1} goto :eof
:patching
if not exist "%outdir%" md "%outdir%"
echo patch_lst is %patch_lst%
call :countlines %patch_lst% total_xdeltas
if %total_xdeltas% GTR 0 (
set /a n=0
for /f "usebackq tokens=*" %%A in (%patch_lst%) do (
set /a n+=1
TITLE Patching !n!/!total_xdeltas!
set filename=%%A
rem echo xdelta "!patchdir!\%%A"
set fileext=%%~xA
set filefolder=%%~dpA
set filefolder=!filefolder:~0,-1!
if {%IMDEBUG%}=={1} (
echo !filename! extension !fileext!
timeout /t 1 > nul
)
rem echo gamesubfolder for the file is !filefolder!
if /i {!fileext!}=={!patchext!} (
set filename=!filename:~0,-%length%!
rem Now filename var contains name of file being patched.
rem It and its relative path are expected to exist
rem echo oldfile "!filename!"
if not {%IMDEBUG%}=={1} (
rem There's no need to create subfolder structure in _new if OVERWRITE_SOURCE_FILES set to 1
rem because the patched temp file if it's good will be moved over old file anyway
rem ===== PATCHER RUN ===== jpatch [options] <original file> <patch file> [<output file>]
%patcher_exe% %patcher_params% "!filename!" "!patchdir!\%%A" "!outdir!\_patched.tmp"
IF !ERRORLEVEL! NEQ 0 (
rem new file in outdir is probably bad - need to delete it to prevent it from overwriting good source
if exist "!outdir!\_patched.tmp" del /q "!outdir!\_patched.tmp" 2>nul
echo ERROR creating patched file "!filename!"
echo %%A>> %notpatched_list%
) else (
rem patch was applied successfully - "!outdir!\_patched.tmp" is valid
if {%OVERWRITE_SOURCE_FILES%}=={1} (
echo Overwriting old file "!filename!" in game folder using patched file from !outdir!
rem move ready (pre)patched file to game folder from _patch
move /y "!outdir!\_patched.tmp" "!filename!"
) else (
echo Preparing subfolder structure for "!outdir!\!filename!"
rem can't use "!outdir!\_patched.tmp" as source because xcopy will consider it a cyclic copy
xcopy "!filename!" "!outdir!\!filename!" /T /-I /Y 2>nul
move /y "!outdir!\_patched.tmp" "!outdir!\!filename!"
)
rem may delete xdelta from patchdir
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} del /q "!patchdir!\%%A"
)
) else (
rem DEBUG PRINT echo xcopy "!filename!" "!outdir!\!filename!" /T /-I /Y 2^>nul
echo %patcher_name% "!filename!"
echo "!patchdir!\%%A"
echo "!outdir!\!filename!"
)
) else (
rem File and/or its relative path may not exist in the game folder
rem if it is a new file .e.g. part of new DLC!
echo Considering "!filename!" ready
if {%OVERWRITE_SOURCE_FILES%}=={1} (
echo Overwriting old file in game folder
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} (
if not exist "!filename!" (
rem prepare subfolder structure for new file or else move command below will fail
xcopy "!patchdir!\%%A" "!filename!" /T /-I /Y
)
rem move ready (pre)patched file to game folder from _patch
move /y "!patchdir!\%%A" "!filename!"
) else (
rem xcopy will create needed subfolder structure if it doesn't exist
xcopy "!patchdir!\%%A" "!filename!" /-I /R /Y
)
) else (
rem Moving/Copying ready file to outdir
rem Needed subfolder structure may not exist in outdir yet!
rem Can't use !filename! as source file in xcopy because it may be new
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} (
echo Preparing subfolder structure before moving
xcopy "!patchdir!\%%A" "!outdir!\!filename!" /T /-I /Y 2>nul
rem echo move /y "!patchdir!\%%A" "!outdir!\!filename!"
move /y "!patchdir!\%%A" "!outdir!\!filename!"
) else (
xcopy "!patchdir!\%%A" "!outdir!\!filename!" /-I /R /Y
)
)
IF !ERRORLEVEL! NEQ 0 (
echo ERROR copying or moving ready file "!filename!"
echo %%A>> %notpatched_list%
)
)
echo ------
)
TITLE Done !n! files
)
rem ---------------------- postpatch finish section begin ----------------------
if exist "%patchdir%\%arcexe_name%" set arcexe="%patchdir%\%arcexe_name%"
if exist "%arcexe_name%" set arcexe="%arcexe_name%"
if exist ExternalData\ExternalData.arc (
if defined arcexe (
%arcexe% x ExternalData\ExternalData.arc --overwrite=+
rmdir /q /s "ExternalData"
) else (
echo ERROR external data unpacker %arcexe_name% not found - unpack ExternalData\ExternalData.arc manually!
)
)
rem to manually create .arc from the folder with files run
rem Arc.exe create {path\to}ExternalData.arc -r -mx -ld3600m -mt14
rem to exclude some folder "-x{patch\to\folder}"
rem ---------------------- postpatch finish section end ------------------------
if {%OVERWRITE_SOURCE_FILES%}=={1} (
rmdir /q /s "!outdir!"
echo Patched files are in their place in your game folder!
echo Verify them using new .sha1 or .md5 or .blake3 file
) else (
echo New patched files are in "!outdir!" folder !
echo Verify them using new .sha1 or .md5 or .blake3 file there.
echo If they are OK then manually move them to game folder
echo and overwrite old files
)
if exist %notpatched_list% (
echo Errors were found during patching in %notpatched_list%:
echo.
type %notpatched_list%
echo.
echo Make sure you have enough space for the patched file^(s^) above and rerun %~nx0
echo On next run %notpatched_list% will be automatically renamed as %patch_lst_name%
echo and keyfile check will be disabled because keyfile might have already been patched
pause
) else (
pause
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} (
echo Deleting %dirtype% patchdir "!patchdir!"
rmdir /q /s "!patchdir!"
) else (
echo Not touching %dirtype% patchdir "!patchdir!"
)
rem %patch_lst_name% %keyfile_txt_name% are in patchdir and handled by DELETE_FILES_FROM_PATCHDIR
if not {%IMDEBUG%}=={1} del /q %patch_lst% %patcher_name% %arcexe_name% patch_readme.txt %0
)
goto :eof
:check_special_file
rem special files will not be added to dynamic xdelta/prepatched list
rem %1 may contain spaces if game subfolders or files have ones
if /I "%~1"=="%patch_lst_name%" exit /B 1
if /I "%~1"=="%keyfile_txt_name%" exit /B 1
if /I "%~1"=="%junked_lst_name%" exit /B 1
if /I "%~1"=="%prepatch_cmd_name%" exit /B 1
if /I "%~1"=="%patcher_name%" exit /B 1
if /I "%~1"=="%arcexe_name%" exit /B 1
exit /B 0
goto :eof
:gen_patch_lst
rem Subroutine to print list of files in %patchdir% with relative paths
for /f "tokens=*" %%A in ("%patchdir%") do (
set path_to_patchdir=%%~fA\
)
if {%IMDEBUG%}=={1} echo gen_patch_lst - path_to_patchdir is %path_to_patchdir% 1>&2
if {%IMDEBUG%}=={1} echo command is dir /b /s /a-d "%patchdir%" 1>&2
rem counting patchdir prefix length
call :stringlength "%path_to_patchdir%" prefix_length
if {%IMDEBUG%}=={1} echo gen_patch_lst - PatchDir path prefix %path_to_patchdir% length is: %prefix_length% 1>&2
for /f "tokens=*" %%A in ('dir /b /s /a-d "%patchdir%"') do (
set fullfilename=%%A
set filename=!fullfilename:~%prefix_length%!
if {%IMDEBUG%}=={1} echo fullfilename is !fullfilename!, filename is !filename! 1>&2
call :check_special_file "!filename!"
IF !ERRORLEVEL! NEQ 0 (
if {%IMDEBUG%}=={1} echo Skipping %%A 1>&2
) else (
echo !filename!
)
)
goto :eof
:countlines
rem Subroutine to calculate number of lines in 1st argument file
rem Result will be assigned to variable NAME specified as 2nd argument
set /a %2=0
for /f "usebackq tokens=*" %%A in (%1) do set /a %2+=1
goto :eof
:stringlength
rem Subroutine to calculate length of string in 1st argument
rem Result will be assigned to variable NAME specified as 2nd argument
set tmpvar=%~1
set /a %2=0
:loop
if defined tmpvar (
rem Action Syntax Example Output (for "BatchFile")
rem First n characters %var:~0,n% %var:~0,5% Batch
rem Skip first n, get rest %var:~n% %var:~5% File
rem Last n characters %var:~-n% %var:~-4% File
rem All but last n %var:~0,-n% %var:~0,-4% Batch
set "tmpvar=%tmpvar:~1%"
set /a %2+=1
goto loop
)
goto :eof
:exiting
pause
Changelog
v1.8 supports arc.exe inside patchdir and deleting folders in junked list
1.8a handles spaces in PatchData subpaths and filesnames
v1.7 Dynamically generates _patch.lst from patchdir.
Supports _keyfile.txt, _junked.lst, _prepatch.cmd inside patchdir.
v1.7a supports patcher exe inside patchdir
v1.6 external _prepatch.cmd and _keyfile.txt, keyfilealgo variable is assigned automatically
v1.5 supports keyfile hash check - specify keyfile, keyfilehash, keyfilealgo variables below
1.5a calculates patchext length
v1.4 supports _junked.lst which can be made from [JUNKED] section of ISXPM checker.ini
v1.3 supports postpatch arc extraction of ExternalData if it is copied into game folder
v1.2 supports custom patchdir specified as 1st parameter on the commandline and catches patcher errors
v1.1 supports overwriting source files for saving space and prepatched files in _patch folder
No comments:
Post a Comment