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 the .bat file (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
- free space checking before applying patch
Now it is a part of USXPM (Unix-Style Xdelta Patch Maker) suite with more features added including free space checking before applying patch.
USXPM_PATCHER.bat
@echo off
set banner=USXPM_PATCHER v3.6
if {%USXPM_MASTER_STARTED%}=={1} (
echo Parallelizing %banner% from USXPM toolkit https://ant-sh.blogspot.com
echo USXPM is UNIX-Style Xdelta Patch Maker by Anton Shabunin
echo ==================================================================
)
set PARALLEL_MODE=1
set LOG_ENABLED=1
rem Applies xdeltas from ISXPM-compatible PatchData folder
rem Formerly ISXPM_MANUAL.bat, now part of UNIX-Style Xdelta Patch Maker (USXPM) suite.
rem %1 (opt) Custom PatchData folder.
rem If not specified subfolder with default name PatchData in current folder is used
rem This parameter is only valid for master instance of the script in parallel mode.
rem Child instances rely on patchdir var set and validated by master and
rem use %1 for its own purposes in :XDELTA_APPLIER.
set SCRIPTDIR=%~dp0
if "%SCRIPTDIR:~-1%"=="\" set SCRIPTDIR=%SCRIPTDIR:~0,-1%
if not {%USXPM_LOGGING_STARTED%}=={1} echo %~n0 running from "%CD%" with parameters %*
if /i not "%CD%"=="%SCRIPTDIR%" (
echo Changing to script dir "%SCRIPTDIR%" so that default patchdir could be found
pushd "%SCRIPTDIR%"
)
if not {%USXPM_LOGGING_STARTED%}=={1} echo Current dir is "%CD%", script dir is "%SCRIPTDIR%"
setlocal enabledelayedexpansion
if {%PARALLEL_MODE%}=={1} (
if {%USXPM_MASTER_STARTED%}=={1} (
rem if defined USXPM_MASTER_STARTED (
echo Running XDELTA_APPLIER instance with parameters %*
goto :XDELTA_APPLIER
) else (
echo Started in parallel master mode with parameters %* - continuing
set USXPM_MASTER_STARTED=1
)
)
rem Master script ---------------------------------
rem set IMDEBUG=1
rem will use fake hpatchz-x64-debug.exe which is renamed timeout.exe
rem set IMDEBUG_FAKE_PATCHER=1
rem will make actual patched tmp files in %outdir%
rem set IMDEBUG_MAKE_PATCHEDFILES=1
REM set NOPAUSE=1
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
rem !!! CLEANUP_AFTER will remove script itself, executables, lists, readme after successful patch application
set OVERWRITE_SOURCE_FILES=1
set DELETE_FILES_FROM_PATCHDIR=1
set CLEANUP_AFTER=1
if {%IMDEBUG%}=={1} echo DEBUG USXPM_MASTER_STARTED is %USXPM_MASTER_STARTED%
rem Parallel xdelta application support
rem It may take up to XDELTA_APPLIER_INSTANCES * mod_size_max free space in _USXPM_new
rem on 8 core CPU good results show
rem 12 XDELTA_APPLIER_INSTANCES with patcher_params -p-1
REM 10 instances with patcher_params -p-2
set /A XDELTA_APPLIER_INSTANCES=%NUMBER_OF_PROCESSORS% - 4
if %XDELTA_APPLIER_INSTANCES% LSS 1 set XDELTA_APPLIER_INSTANCES=2
rem if {%IMDEBUG%}=={1} set XDELTA_APPLIER_INSTANCES=14
set APPLIER_INSTANCE_CHECK_INTERVAL=5
rem /belownormal for launched cmd.exe instances may delay their startup due to lower priority
rem set APPLIER_INSTANCE_PRIORITY=/belownormal
rem PatchData is default folder name in ISXPM
set patchdir=PatchData
set external_data_foldername=ExternalData
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
if {%IMDEBUG_FAKE_PATCHER%}=={1} set patcher_name=hpatchz-x64-debug.exe
if {%IMDEBUG_FAKE_PATCHER%}=={1} (
rem patcher_name is ...-debug.exe now and won't be found in PatchData but in %TEMP%.
rem Need to set patcher_exe here so that script could make into main loop
if not exist "%TEMP%\%patcher_name%" copy /y "%WINDIR%\system32\timeout.exe" "%TEMP%\%patcher_name%"
set patcher_exe="%TEMP%\%patcher_name%"
)
rem multi-thread single-file parallel mode -p-5
rem supports single compressed diffData(created by hdiffz -SD-stepSize)
rem or window diffData(HDIFFW26 created by -WD[-stepSize], needs v5.0 patcher)
set DEFAULT_PATCHER_PARAMS=-p-5 -f
set patcher_params=%DEFAULT_PATCHER_PARAMS%
if {%PARALLEL_MODE%}=={1} set patcher_params=-p-1 -f
rem for xdelta3 set patcher_params=-d -s
set unarcexe_name=unarc.exe
set outdir=_USXPM_new
rem Dynamically generate _patch.lst from patchdir.
rem Only done if there are no explicit _patch.lst files in game or PatchData folders.
rem Possible issue without explicit _patch.lst - if old xdeltas were left over in PatchData
rem then gen_patch_lst will include them into dynamic patch list, they will be rejected
rem and reported as errors!
set DYNAMIC_PATCH_LST=1
rem Save patcher commands for selective manual application if needed
set MAKE_MANUAL_COMMAND_LIST=1
set manual_command_file="%TEMP%\%~n0_command_list.txt"
rem lockfile for appending to the same error filelist by multiple instances.
rem also for errors in xcopy of ready files
set notpatched_semaphore="%TEMP%\%~n0_notpatched.lock"
if exist %notpatched_semaphore% del /q %notpatched_semaphore%
rem lockfile for appending to notpatched_command_list. only for xdeltas
set notpatched_command_semaphore="%TEMP%\%~n0_notpatched_command.lock"
if exist %notpatched_command_semaphore% del /q %notpatched_command_semaphore%
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 considered MD5
rem 40 byte hash is considered SHA1
rem These executables may only be needed under WINE which has no fully implemented certutil
set md5exe_name=md5sum.exe
set sha1exe_name=sha1sum.exe
rem if file in _patch folder (patchdir variable) is not an xdelta but
rem instead a ready file without %patchext% then it will be moved to _USXPM_new (outdir variable)
rem or directly to destination if OVERWRITE_SOURCE_FILES set to 1
set notpatched_list=_patch.bad
rem list of patcher commands that reported failed. will be recreated on each run
set notpatched_command_list=_patch_commands.bad
set patch_lst_name=_patch.lst
set keyfile_txt_name=_keyfile.txt
set junked_lst_name=_junked.lst
rem Put commands to rearrange/rename/back up files before patching into _prepatch.cmd for example
rem if exist "test.txt" (
rem md new\folder\where\it\was\relocated
rem move test.txt new\folder\where\it\was\relocated\test_renamed.txt
rem )
set prepatch_cmd_name=_prepatch.cmd
rem File with free space requirements - CSV with columns.
rem Tirst two columns are mandatory and cannot be empty.
rem 1 2 3 (opt) 4 (opt)
rem mod_size_max mod_size_total junked_size_total added_size_total
rem (largest modified (size of all (size of files (size of new files)
rem file) modified files) to be deleted)
set freespace_req_name=_freespace_req.txt
set bincalcexe_name=USXPM_bc.exe
set branding_file_name=_patch.nfo
rem Custom patchdir support
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 =========================
set loggerexe_name=USXPM_tee.exe
if not %LOG_ENABLED%==1 goto :main
echo %TIME% Logging enabled
rem Check if the script is already running in logging mode
if {%USXPM_LOGGING_STARTED%}=={1} (
echo Logging already started in the parent process
goto :main
)
echo Setting up logging - not yet started
if exist "%patchdir%\%loggerexe_name%" set logger_exe="%patchdir%\%loggerexe_name%"
if exist "%loggerexe_name%" set logger_exe="%loggerexe_name%"
if not defined logger_exe (
echo WARNING logger %loggerexe_name% not found - continuing without logging
set LOG_ENABLED=0
goto :main
)
echo Relaunching with logging - %~n0%~x0 %*
rem pause
set USXPM_LOGGING_STARTED=1
rem starting parallel master anew
if {%PARALLEL_MODE%}=={1} set USXPM_MASTER_STARTED=0
rem "%~f0" 2>&1
rem start "LOGGER"
cmd /c %~n0%~x0 %* | %logger_exe% "%~dp0%~n0.log"
rem echo Now need to clean up
rem set NOPAUSE=1
rem goto :cleanup
exit /b
:main
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
)
set RUNNING_UNDER_WINE=0
rem findstr /i "wine_get_version" "%SystemRoot%\system32\ntdll.dll" >nul 2>&1
rem may not find Wine specific export string depending on iocharset of mounted fs
reg query "HKLM\Software\Wine" >nul 2>&1
if %errorlevel% equ 0 (
set RUNNING_UNDER_WINE=1
echo Running under WINE - %patchdir% must not have spaces or move hashers beside the script
rem assume that hashing exes are on PATH
set md5exe=%md5exe_name%
set sha1exe=%sha1exe_name%
if exist "%patchdir%\%md5exe_name%" set md5exe=%patchdir%\%md5exe_name%
if exist "%patchdir%\%sha1exe_name%" set sha1exe=%patchdir%\%sha1exe_name%
)
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 ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
echo ============================ DEBUG IS ENABLED ===========================
echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
echo keyfile_txt is %keyfile_txt%
echo keyfile is %keyfile%, expected keyfilehash %keyfilehash%
)
if exist %notpatched_list% (
echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
echo ====================== RETRYING PATCH MODE =============================
echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
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.
pause
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=
if {%PARALLEL_MODE%}=={1} (
echo Disabling parallel mode for repeat patch.
set PARALLEL_MODE=0
set patcher_params=%DEFAULT_PATCHER_PARAMS%
)
)
rem list of patcher commands that reported failed will be recreated on each run
if exist %notpatched_command_list% del /q %notpatched_command_list%
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 check if it is identification by size for big keyfile SIZE_{keyfilesize}
if /i "%keyfilehash:~0,5%"=="SIZE_" (
echo Keyfile is identified by size
for /f "tokens=*" %%B in ("%keyfile%") do (
if "%%~zB"=="%keyfilehash:~5%" (
echo Keyfile size %%~zB bytes match OK - continuing
) else (
echo ERROR Keyfile %keyfile% specified but size mismatches
echo found %%~zB bytes but expected %keyfilehash:~5% bytes - exiting
goto :exiting
)
)
) else (
rem Determine keyfilealgo
call :stringlength %keyfilehash% keyfilehashlen
if !keyfilehashlen!==40 (
if {%IMDEBUG%}=={1} echo detected keyfilealgo SHA1
set keyfilealgo=SHA1
set wine_hasher=%sha1exe%
) else (
if !keyfilehashlen!==32 (
if {%IMDEBUG%}=={1} echo detected keyfilealgo MD5
set keyfilealgo=MD5
set wine_hasher=%md5exe%
)
)
if defined keyfilealgo (
if {%RUNNING_UNDER_WINE%}=={1} (
if {%IMDEBUG%}=={1} echo Running !wine_hasher! "%keyfile%"
for /f "usebackq tokens=1,2" %%h in (`!wine_hasher! "%keyfile%"`) 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 (
rem certutil hash algorithms: MD2 MD4 MD5 SHA1 SHA256 SHA384 SHA512
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 checking branding file - only after keyfile is good
if exist "file_id.diz" set branding_file="file_id.diz"
if exist "%patchdir%\%branding_file_name%" set branding_file="%patchdir%\%branding_file_name%"
echo branding_file is %branding_file%
set proceed=n
if defined branding_file (
echo.
echo ==================================================================================
echo Powered by parallelizing %banner% from USXPM https://ant-sh.blogspot.com
echo ==================================================================================
echo.
type %branding_file%
echo.
set /P "proceed=Do you want to continue with the update? (y/n): "
if /I not {!proceed!}=={y} goto :eof
) else (
echo ============================================================================
echo Parallelizing %banner% from USXPM toolkit https://ant-sh.blogspot.com
echo USXPM is UNIX-Style Xdelta Patch Maker by Anton Shabunin
echo ============================================================================
)
rem counting extension length
call :stringlength %patchext% patchext_length
echo Xdelta extension is %patchext%, length %patchext_length%
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 Check if free space requirements are available for this patch
if exist "%patchdir%\%freespace_req_name%" set freespace_req_file="%patchdir%\%freespace_req_name%"
echo freespace_req_file is %freespace_req_file%
if not defined freespace_req_file (
echo WARNING no special file %freespace_req_name% with free space requirements found
if {%PARALLEL_MODE%}=={1} (
echo Free space estimation for parallel processing is not available - disabling parallel mode
set PARALLEL_MODE=0
set patcher_params=%DEFAULT_PATCHER_PARAMS%
)
goto :determine_patch_list
)
rem find bincalc
if exist "%patchdir%\%bincalcexe_name%" set bincalc_exe="%patchdir%\%bincalcexe_name%"
if exist "%bincalcexe_name%" set bincalc_exe="%bincalcexe_name%"
if not defined bincalc_exe (
echo Bin calc executable %bincalcexe_name% not found
echo - no free space estimation for parallel processing
echo - no human-readable free space reporting
)
set mod_size_max=0
set mod_size_total=0
set junked_size_total=0
set added_size_total=0
set freespace_req_file_line=0
set parallel_est_expression=0
rem space is needed in delims as freespace_req_file will have space at the end
for /f "usebackq tokens=1,2,3,4 delims=, " %%A in (%freespace_req_file%) do (
if !freespace_req_file_line!==0 (
set mod_size_max=%%A
set mod_size_total=%%B
set junked_size_total=%%C
set added_size_total=%%D
) else (
if !freespace_req_file_line! LEQ %XDELTA_APPLIER_INSTANCES% (
rem construct bc expression to calculate total estimated size for XDELTA_APPLIER_INSTANCES instances
rem echo line !freespace_req_file_line! size %%A
if !freespace_req_file_line!==1 (
set parallel_est_expression=%%A
) else (
set parallel_est_expression=!parallel_est_expression!+%%A
)
)
)
set /a freespace_req_file_line+=1
)
rem echo freespace_req_file_line is %freespace_req_file_line%
rem echo %mod_size_max%,%mod_size_total%,%junked_size_total%,%added_size_total%
rem mod_size_parallel is dynamic because each PC has different number of CPU cores %XDELTA_APPLIER_INSTANCES% is based on
set mod_size_parallel=0
if {%PARALLEL_MODE%}=={1} (
if defined bincalc_exe (
if %freespace_req_file_line%==1 (
echo WARNING No expanded size info in %freespace_req_file%
echo - free space estimation for %XDELTA_APPLIER_INSTANCES% parallel instances may be not exact
rem need (n+0.5)/1 to remove decimals
for /f %%A in ('echo ^(^(%mod_size_max%*%XDELTA_APPLIER_INSTANCES%*0.8^)+0.5^)/1 ^| %bincalc_exe%') do set mod_size_parallel=%%A
rem TODO may recreate expanded size info for bincalc from xdeltas using hpatchz-x64.exe -info
rem fileName: "0.paz._hdif_"
rem ...
rem saved newDataSize: 953919904
rem need to move whole freespace_req block below dynamic patch_lst is ready but before :patching
) else (
if {%IMDEBUG%}=={1} echo parallel_est_expression is %parallel_est_expression%
for /f %%A in ('echo %parallel_est_expression% ^| %bincalc_exe%') do set mod_size_parallel=%%A
)
)
)
rem determine free space
set free_space=0
for /f "tokens=3" %%A in ('dir /-C') do set free_space=%%A
rem WMIC may be not installed --- for /f "tokens=2 delims==" %%A in ('wmic logicaldisk where "DeviceID='D:'" get FreeSpace /value') do set free_space=%%A
rem Take the last 20 characters of each for comparison as strings
set "pad1= %free_space%"
set "str1=%pad1:~-20%"
if {%OVERWRITE_SOURCE_FILES%}=={1} (
if {%PARALLEL_MODE%}=={1} (
rem echo Parallel mode enabled, it may require up to %XDELTA_APPLIER_INSTANCES% X %mod_size_max% bytes.
if not %mod_size_parallel%==0 (
call :get_max %mod_size_parallel% %added_size_total% size_to_check_for
) else (
echo WARNING No expanded size info in %freespace_req_file% or no %bincalcexe_name% found
echo Estimation of free space for parallel processing is not available - parallel mode DISABLED
set PARALLEL_MODE=0
set patcher_params=%DEFAULT_PATCHER_PARAMS%
call :get_max %mod_size_max% %added_size_total% size_to_check_for
)
) else (
rem only need space for max size file (%mod_size_max%) or new files
call :get_max %mod_size_max% %added_size_total% size_to_check_for
)
) else (
rem need space for full set of modified files (%mod_size_total%) or new files
call :get_max %mod_size_total% %added_size_total% size_to_check_for
)
set "pad2= %size_to_check_for%"
set "str2=%pad2:~-20%"
if {%IMDEBUG%}=={1} echo str1 free space is %str1%, str2 requirement is %str2%
if "%str1%" GTR "%str2%" (
echo Free space %free_space% bytes OK for %size_to_check_for% bytes required
) else (
if defined bincalc_exe (
for /f %%A in ('echo scale^=2^; %free_space% / ^(1024*1024*1024^) ^| %bincalc_exe%') do (
for /f %%B in ('echo scale^=2^; %size_to_check_for% / ^(1024*1024*1024^) ^| %bincalc_exe%') do (
echo Got %%A Gb but needed at least %%B Gb free
for /f %%C in ('echo scale^=2^; ^(%size_to_check_for%-%free_space%^) / ^(1024*1024*1024^) ^| %bincalc_exe%') do (
echo ERROR not enough free space for patch - exiting
echo.
echo You need to free up at least %%C Gb more
if {%PARALLEL_MODE%}=={1} (
echo OR
echo You may reduce XDELTA_APPLIER_INSTANCES in parallel mode, currently it is %XDELTA_APPLIER_INSTANCES%
echo OR
echo Disable parallel mode
echo.
)
)
)
)
) else (
echo Got %free_space% but needed at least %size_to_check_for% bytes free
echo ERROR not enough free space for patch - exiting
)
goto :exiting
)
:determine_patch_list
rem if {%IMDEBUG%}=={1} goto :eof
rem explicit _patch.lst in game folder takes precedence.
if exist "%patch_lst_name%" (
echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
echo ====================== EXPLICIT %patch_lst_name% MODE =================
echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
set patch_lst="%patch_lst_name%"
set DYNAMIC_PATCH_LST=0
goto :patching
)
rem if explict _patch.lst is in PatchData folder put by USXPM_PATCHDATA_MAKER.bat then use it.
rem Means to safeguard against old leftover xdeltas
if exist "%patchdir%\%patch_lst_name%" (
echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
echo ============== EXPLICIT %patchdir%\%patch_lst_name% MODE ================
echo ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
set patch_lst="%patchdir%\%patch_lst_name%"
set DYNAMIC_PATCH_LST=0
goto :patching
)
rem make temporary dynamic lst unique so that multiple patches could be run simultaneously
set patch_lst="%TEMP%\%RANDOM%%patch_lst_name%"
if {%IMDEBUG%}=={1} set patch_lst="%TEMP%\DEBUG%patch_lst_name%"
if {%DYNAMIC_PATCH_LST%}=={1} (
if {%IMDEBUG%}=={1} echo Dynamically making patchlist in !patch_lst!
rem Delayed expansion of !patch_lst! set in this if {...} doesn't work under WINE here
rem and errors witn Invalid name without calling gen_patch_lst
rem (call :gen_patch_lst) > !patch_lst!
(call :gen_patch_lst) > %patch_lst%
if {%IMDEBUG%}=={1} echo Made patchlist in !patch_lst!
)
:patching
call :countlines %patch_lst% total_xdeltas_or_files
echo DYNAMIC_PATCH_LST is %DYNAMIC_PATCH_LST%
echo patch_lst is %patch_lst%
echo total_xdeltas_or_files is %total_xdeltas_or_files%
rem set prepatch prepare script
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%
rem n is a counter which includes prepatched/ready files
rem num_patched_files is actual number of successfully applied xdeltas
rem can't easily track num_patched_files with parallel single file patchers in separate cmd instances.
rem It is possible to collect successfully patched filenames into a file and count lines in it.
rem It will require another semaphore and at least 3 file modifications for each applied xdelta:
rem ( 1 semaphore create + 2 delete + 3update the successfully patched filenames list ).
rem It may be too write intensive for big patches
set /a n=0
set /a num_patched_files=0
set /a tried_patched_files=0
if %total_xdeltas_or_files% GTR 0 (
rem Running prepatch prepare script only if there's something to do
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%
)
)
echo %TIME% ------------------- Main processing loop started ----------------------
if not exist "%outdir%" md "%outdir%"
rem Command file for selective manual application is recreated on each run and not deleted
if {%MAKE_MANUAL_COMMAND_LIST%}=={1} echo pushd "%CD%" > %manual_command_file%
set /a running_applier_instances=0 > nul
for /f "usebackq tokens=*" %%A in (%patch_lst%) do (
set /a n+=1
TITLE Patching !n!/!total_xdeltas_or_files!
set filename=%%~A
rem echo xdelta "!patchdir!\%%A"
set fileext=%%~xA
rem this will include full path to current folder - not applicable rem set filefolder=%%~dpA rem set filefolder=!filefolder:~0,-1!
echo Processing file !n! - !filename! extension !fileext!
rem timeout /t 1 > nul
call :stringlength "\%%~nA%%~xA" filename_with_ext_length
if {%IMDEBUG%}=={1} echo filename_with_ext_length !filename_with_ext_length!
set filefolder=%%~A
for /f %%B in ("!filename_with_ext_length!") do set filefolder=!filefolder:~0,-%%B!
rem this !filefolder! contains relative subfolder structure
if /i {!fileext!}=={!patchext!} (
set /a tried_patched_files+=1
rem track commands for manual selective reapply
if {%MAKE_MANUAL_COMMAND_LIST%}=={1} (
echo rem ============== file !n! - xdelta # !tried_patched_files! >> %manual_command_file%
echo if not exist "%outdir%\!filefolder!" md "%outdir%\!filefolder!" >> %manual_command_file%
echo %patcher_exe% %DEFAULT_PATCHER_PARAMS% "!filename:~0,-%patchext_length%!" "!patchdir!\%%~A" "%outdir%\!filename:~0,-%patchext_length%!">> %manual_command_file%
)
if {%PARALLEL_MODE%}=={1} (
if !running_applier_instances! GEQ %XDELTA_APPLIER_INSTANCES% (
rem if !tried_patched_files! GTR %XDELTA_APPLIER_INSTANCES%
call :wait_for_applier_vacancy
if {%IMDEBUG%}=={1} echo Main loop: === after wait_for_applier_vacancy !running_applier_instances!/%XDELTA_APPLIER_INSTANCES% instances
)
if {%IMDEBUG%}=={1} echo Main loop: !running_applier_instances!/%XDELTA_APPLIER_INSTANCES% instances - can launch another instance
echo !TIME! launching parallel xdelta applier # !tried_patched_files! for %%A
rem if script started by doubleclicking in File Explorer it will be started as
rem C:\WINDOWS\system32\cmd.exe /c ""full\path\to\USXPM_PATCHER.bat" "
rem $0 will be set to "full\path\to\USXPM_PATCHER.bat" and start command below will fail !!!
if {%IMDEBUG%}=={1} echo start "XDELTA_APPLIER" /min %APPLIER_INSTANCE_PRIORITY% CMD.exe /c %~n0%~x0 "%%~A"
rem start "XDELTA_APPLIER" /min CMD /c "%~f0" %%A
start "XDELTA_APPLIER" /min %APPLIER_INSTANCE_PRIORITY% CMD.exe /c %~n0%~x0 "%%~A"
set /a running_applier_instances+=1
set /a check_appliers = !tried_patched_files! %% 7
rem if {%IMDEBUG%}=={1} echo check_appliers is !check_appliers!
if !check_appliers!==0 (
echo Throttling instance launches
timeout /t 1 /nobreak > nul
)
rem wait so that new single file patcher process is detected by tasklist
if !tried_patched_files!==3 call :wait_for_applier_launch
rem if {%IMDEBUG%}=={1} pause
) else (
echo Applying xdelta for %%A sequentially
call :XDELTA_APPLIER "%%~A"
)
) else (
echo Considering "!filename!" ready
if {%MAKE_MANUAL_COMMAND_LIST%}=={1} (
echo rem ============== file !n! - ready >> %manual_command_file%
echo if not exist "%outdir%\!filefolder!" md "%outdir%\!filefolder!" >> %manual_command_file%
echo xcopy "!patchdir!\%%~A" "%outdir%\!filefolder!" /I /R /Y >> %manual_command_file%
)
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!
REM can use relative !filefolder! now to make subfolder structure
if {%OVERWRITE_SOURCE_FILES%}=={1} (
echo Overwriting old file in game folder
if not {%IMDEBUG%}=={1} (
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} (
if not exist "!filename!" (
rem prepare subfolder structure for new file or else move command below will fail
rem WINE doesn't support /-I and may ask if it is a folder or a file. Need to press F
rem --- xcopy "!patchdir!\%%A" "!filename!" /T /-I /Y
for /f "tokens=*" %%B in ("!filename!") do (
if not exist "%%~dpB" md "%%~dpB"
)
)
rem move ready (pre)patched file to game folder from _patch
if {%IMDEBUG%}=={1} echo move /y "!patchdir!\%%A" "!filename!"
rem May not work under WINE with filenames that have spaces
move /y "!patchdir!\%%~A" "!filename!"
) else (
rem xcopy will create needed subfolder structure if it doesn't exist
rem WINE doesn't support /-I and may ask if it is a folder or a file. Need to press F
rem xcopy "!patchdir!\%%A" "!filename!" /-I /R /Y
for /f "tokens=*" %%B in ("!filename!") do (
if not exist "%%~dpB" md "%%~dpB"
xcopy "!patchdir!\%%~A" "%%~dpB" /I /R /Y
)
)
) else (
echo DEBUG move or copy "!patchdir!\%%~A" as "!filename!"
)
) 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 not {%IMDEBUG%}=={1} (
for /f "tokens=*" %%B in ("!outdir!\!filename!") do (
if not exist "%%~dpB" md "%%~dpB"
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} (
echo Preparing subfolder structure before moving
rem WINE has no /-I
rem --- 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 (
rem WINE has no /-I
rem --- xcopy "!patchdir!\%%A" "!outdir!\!filename!" /-I /R /Y
xcopy "!patchdir!\%%~A" "%%~dpB" /I /R /Y
)
)
) else (
echo Not overwriting old file in game folder
echo DEBUG move or copy "!patchdir!\%%A" as "!outdir!\!filename!"
)
)
IF !ERRORLEVEL! NEQ 0 (
echo ERROR copying or moving ready file "!filename!"
if {%PARALLEL_MODE%}=={1} call :semaphore_get %notpatched_semaphore%
echo %%A>> %notpatched_list%
if {%PARALLEL_MODE%}=={1} call :semaphore_release %notpatched_semaphore%
)
)
echo ------
)
if {%PARALLEL_MODE%}=={1} (
TITLE Wait...
) else (
TITLE Done !n! files
)
rem Avoid cluttering %TEMP%
if not {%IMDEBUG%}=={1} if {%DYNAMIC_PATCH_LST%}=={1} del /q %patch_lst%
)
rem at 3rd patch instance launch it waits for patcher exe process to appear
rem if there's only 1 xdelta it will never wait and may finish the script before child instance
rem In this case need to wait for patcher exe process here
if %tried_patched_files% LSS 3 call :wait_for_applier_launch
rem Wait for finish of all applier instances
set XDELTA_APPLIER_INSTANCES=1
set /a APPLIER_INSTANCE_CHECK_INTERVAL/=2
if {%PARALLEL_MODE%}=={1} (
echo Reduced APPLIER_INSTANCE_CHECK_INTERVAL is now %APPLIER_INSTANCE_CHECK_INTERVAL%
call :wait_for_applier_vacancy
TITLE Done !n! files
rem Need to unset just in case of another run in the same cmd environment
set USXPM_MASTER_STARTED=0
)
echo %TIME% ------------------- Main processing loop finished ----------------------
if {%IMDEBUG%}=={1} goto :eof
rem Here %n% contains actual number of xdeltas/ready files processed
if {%n%}=={0} (
echo Expected to process %total_xdeltas_or_files% total xdeltas or files
echo ERROR nothing was processed - exiting
goto :exiting
)
rem else (
rem if {%num_patched_files%}=={0} (
rem echo ERROR no xdeltas were applied - exiting
rem goto :exiting
rem )
rem )
rem num_patched_files = tried_patched_files - num_xdelta_errors
rem ---------------------- postpatch finish section begin ----------------------
if exist "%patchdir%\%unarcexe_name%" set arcexe="%patchdir%\%unarcexe_name%"
if exist "%unarcexe_name%" set arcexe="%unarcexe_name%"
set ExternalDataFolder=%external_data_foldername%
rem if custom PatchData location specified look for ExternalData beside it too
if "%dirtype%"=="custom" (
rem DELETE_FILES_FROM_PATCHDIR is 0 as custom PatchData and ExternalData may be archival copies
rem set patchdir=%~1
if not exist "%ExternalDataFolder%" (
echo No default ExternalDataFolder found - trying beside %dirtype% %patchdir%
set ExternalDataFolder=%patchdir%\..\%external_data_foldername%
) else (
echo Using default ExternalDataFolder %ExternalDataFolder%
)
)
if not {%OVERWRITE_SOURCE_FILES%}=={1} set unarc_destination_path=-dp"%outdir%"
if exist "%ExternalDataFolder%\ExternalData.arc" (
if defined arcexe (
TITLE Wait...
%arcexe% x -o+ %unarc_destination_path% "%ExternalDataFolder%\ExternalData.arc"
rem %arcexe% x ExternalData\ExternalData.arc --overwrite=+
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} rmdir /q /s "%ExternalDataFolder%"
TITLE Done
) else (
echo ERROR external data unpacker %unarcexe_name% not found - unpack ExternalData\ExternalData.arc manually!
)
)
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
)
:cleanup
if exist %notpatched_list% (
echo Errors were found during patching in %notpatched_list%:
echo.
type %notpatched_list%
echo.
echo Possible reasons are:
echo - Corrupted or modded files which are rejected by single-file patcher executable
echo.
echo - "!patchdir!" folder has old leftover xdeltas from previous patches.
echo Since the files for those xdeltas now have modified or new data which differs from
echo what they expect they WILL fail to apply.
echo Note timestamps for xdeltas in current patch you are trying to install.
echo Find xdeltas which are older than noted timestamps and delete them.
echo.
echo - Not enough space for the patched file^(s^) above.
echo Now that _freespace_req.txt is present in the patch this is rare and may only
echo happen if some other background process took up space after free space check was done.
echo.
echo Rerun %~nx0 - on next run %notpatched_list% will be automatically
echo renamed as %patch_lst_name% and keyfile check will be disabled
echo because keyfile might have already been patched
if not {%NOPAUSE%}=={1} pause
) else (
if not {%NOPAUSE%}=={1} pause
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} (
if {%USXPM_LOGGING_STARTED%}=={1} taskkill /f /im %loggerexe_name%
echo Deleting %dirtype% patchdir "!patchdir!"
rd /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 {%CLEANUP_AFTER%}=={1} if not {%IMDEBUG%}=={1} del /q %patch_lst% %patcher_name% %unarcexe_name% patch_apply_readme.txt %0
)
goto :eof
rem ---------- actual end of the script, only subroutines are below ---------
: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"=="%unarcexe_name%" exit /B 1
if /I "%~1"=="%freespace_req_name%" exit /B 1
if /I "%~1"=="%md5exe_name%" exit /B 1
if /I "%~1"=="%sha1exe_name%" exit /B 1
if /I "%~1"=="%bincalcexe_name%" exit /B 1
if /I "%~1"=="%loggerexe_name%" exit /B 1
if /I "%~1"=="%branding_file_name%" exit /B 1
exit /B 0
goto :eof
:gen_patch_lst
if {%IMDEBUG%}=={1} echo gen_patch_lst started 1>&2
rem Subroutine to print list of files in %patchdir% with relative paths
for /f "tokens=*" %%A in ("%patchdir%") do (
set path_to_patchdir=%%~fA\
)
rem if {%IMDEBUG%}=={1} echo gen_patch_lst - path_to_patchdir is %path_to_patchdir% 1>&2
rem 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
rem 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%!
rem 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 special file %%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
:get_max
rem Subroutine to determine maximum of 1st and 2nd arguments
rem Result will be assigned to variable NAME specified as 3rd argument
set "get_maxpad1= %~1"
set "get_maxstr1=%get_maxpad1:~-20%"
set "get_maxpad2= %~2"
set "get_maxstr2=%get_maxpad2:~-20%"
if {%IMDEBUG%}=={1} echo str1 is %get_maxstr1%, str2 is %get_maxstr2%
if "%get_maxstr1%" GTR "%get_maxstr2%" (
set %3=%~1
) else (
set %3=%~2
)
goto :eof
:XDELTA_APPLIER
rem This subroutine started when script instance is a child of USXPM master instance
rem %1 is a relative xdelta filename.
rem The rest are derived from global vars set by master including patchext_length var
rem It should do whole xdelta apply sequence because big patchlist loop context will be lost
rem when another entry in the list starts patching/copying
rem - patch applier
rem - errorlevel proc, append to separate notpatched_list using lockfile
rem - if good
rem - move ready file into place or into _USXPM_new (outdir)
rem - delete xdelta from patchdir
rem can't use simple %RANDOM% here because it takes the same value in child instances
if {%PARALLEL_MODE%}=={1} (
rem set /a patched_tmp_num=%RANDOM% + %tried_patched_files%
rem set patched_tmp_file=_patched%patched_tmp_num%.tmp
set patched_tmp_file=_patched%tried_patched_files%.tmp
) else (
set patched_tmp_file=_patched.tmp
)
set filename=!filename:~0,-%patchext_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!"
rem if not {%IMDEBUG%}=={1} (
rem There's no need to create subfolder structure in _USXPM_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>]
echo XDELTA_APPLIER runs %patcher_exe% %patcher_params% "!filename!" "!patchdir!\%~1" "!outdir!\%patched_tmp_file%"
if {%IMDEBUG%}=={1} (
echo %patcher_exe% %patcher_params% "!filename!" "!patchdir!\%~1" "!outdir!\%patched_tmp_file%" > "%TEMP%\DEBUG_%tried_patched_files%.txt"
if {%IMDEBUG_FAKE_PATCHER%}=={1} (
echo DEBUG printing only - no actual files patched
rem This big command doesn't work under WINE - need to split it - (%RANDOM% %% (max-min+1)) + min
rem from 15 to 20 set /a "num=(%RANDOM% %% 6) + 15"
if {%PARALLEL_MODE%}=={1} (
set /a debug_delay=%RANDOM% %% 5
set /a debug_delay+=7
) else (
set /a debug_delay=%RANDOM% %% 2
set /a debug_delay+=1
)
echo Applying xdelta for %* - !debug_delay! s
echo %patcher_name% "!filename!"
echo "!patchdir!\%~1"
echo "!outdir!\%patched_tmp_file%"
%patcher_exe% /t !debug_delay! /nobreak > nul
rem echo DEBUG DELAY d:\temp\hpatchz-x64.exe /t 120
) else (
if {%IMDEBUG_MAKE_PATCHEDFILES%}=={1} %patcher_exe% %patcher_params% "!filename!" "!patchdir!\%~1" "!outdir!\%patched_tmp_file%" >> "%TEMP%\DEBUG_%tried_patched_files%.txt" 2>&1
)
) else (
rem Uncomment line below to create series of "%TEMP%\DEBUG_....txt" files with patcher commands executed
rem echo %patcher_exe% %patcher_params% "!filename!" "!patchdir!\%~1" "!outdir!\%patched_tmp_file%" > "%TEMP%\DEBUG_%tried_patched_files%.txt"
%patcher_exe% %patcher_params% "!filename!" "!patchdir!\%~1" "!outdir!\%patched_tmp_file%"
)
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_file%" del /q "!outdir!\%patched_tmp_file%" 2>nul
echo ERROR creating patched file "!filename!"
if {%PARALLEL_MODE%}=={1} call :semaphore_get %notpatched_semaphore%
echo %~1>> %notpatched_list%
if {%PARALLEL_MODE%}=={1} call :semaphore_release %notpatched_semaphore%
rem save failed command too
if {%PARALLEL_MODE%}=={1} call :semaphore_get %notpatched_command_semaphore%
echo %patcher_exe% %patcher_params% "!filename!" "!patchdir!\%~1" "!outdir!\%patched_tmp_file%" >> %notpatched_command_list%
if {%PARALLEL_MODE%}=={1} call :semaphore_release %notpatched_command_semaphore%
) else (
if {%IMDEBUG%}=={1} goto :eof
rem patch was applied successfully - "!outdir!\%patched_tmp_file%" 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_file%" "!filename!"
) else (
echo Preparing subfolder structure for "!outdir!\!filename!"
rem can't use "!outdir!\%patched_tmp_file%" as source because xcopy will consider it a cyclic copy
rem WINE has no /-I parameter
rem --- xcopy "!filename!" "!outdir!\!filename!" /T /-I /Y 2>nul
for /f "tokens=*" %%B in ("!outdir!\!filename!") do (
if not exist "%%~dpB" md "%%~dpB"
)
move /y "!outdir!\%patched_tmp_file%" "!outdir!\!filename!"
)
rem may delete xdelta from patchdir
if {%DELETE_FILES_FROM_PATCHDIR%}=={1} del /q "!patchdir!\%~1"
)
rem ) else (
rem )
goto :eof
:wait_for_applier_launch
rem waits for at least one %patcher_name% process
rem for 10 seconds. If there's no process appears then it may have already finished
set max_cycles=10
set /a cur_cycle+=1
if {%IMDEBUG%}=={1} echo %TIME% wait_for_applier_launch start - patcher_name %patcher_name%
set /a running_applier_instances=0 > nul
rem tasklist in Windows prints 1 line message to stdout if no processes found - need to use findstr
rem tasklist in WINE a) can't use * in IMAGENAME match like in Windows. Matching is case insensitive
rem b) doesn't print anything if not processes found - no need for findstr
for /f %%C in ('tasklist /nh /fi "IMAGENAME eq %patcher_name%" ^| findstr /I /C:"%patcher_name%"') do (
set /a running_applier_instances+=1 > nul
)
if {%IMDEBUG%}=={1} echo wait_for_applier_launch found %running_applier_instances% instances at start
if %running_applier_instances% GTR 0 (
goto :eof
) else (
if {%IMDEBUG%}=={1} echo Waiting for instance to appear - cycle %cur_cycle%
rem %APPLIER_INSTANCE_CHECK_INTERVAL%
timeout /t 1 > nul
if %cur_cycle% GTR %max_cycles% goto :eof
goto :wait_for_applier_launch
)
goto :eof
:wait_for_applier_vacancy
if {%IMDEBUG%}=={1} echo %TIME% wait_for_applier_vacancy start - patcher_name %patcher_name%
set /a running_applier_instances=0 > nul
rem tasklist in Windows prints 1 line message to stdout if no processes found - need to use findstr
rem tasklist in WINE a) can't use * in IMAGENAME match like in Windows. Matching is case insensitive
rem b) doesn't print anything if not processes found - no need for findstr
for /f %%C in ('tasklist /nh /fi "IMAGENAME eq %patcher_name%" ^| findstr /I /C:"%patcher_name%"') do (
set /a running_applier_instances+=1 > nul
)
if {%IMDEBUG%}=={1} echo wait_for_applier_vacancy found %running_applier_instances% of max %XDELTA_APPLIER_INSTANCES% instances at start
if %running_applier_instances% LSS %XDELTA_APPLIER_INSTANCES% (
if %XDELTA_APPLIER_INSTANCES%==1 (
echo XDELTA_APPLIER instances running: %running_applier_instances%, limit is %XDELTA_APPLIER_INSTANCES% - finished
) else (
echo XDELTA_APPLIER instances running: %running_applier_instances%, limit is %XDELTA_APPLIER_INSTANCES% - OK to add another instance
)
goto :eof
) else (
if %XDELTA_APPLIER_INSTANCES%==1 (
echo Waiting %APPLIER_INSTANCE_CHECK_INTERVAL%s for %running_applier_instances% XDELTA_APPLIER instances to finish
) else (
echo Waiting %APPLIER_INSTANCE_CHECK_INTERVAL%s for some of %running_applier_instances% XDELTA_APPLIER instances to finish
)
timeout /t %APPLIER_INSTANCE_CHECK_INTERVAL% > nul
goto :wait_for_applier_vacancy
)
goto :eof
rem Without locking can't append to the same file at the same time reliably.
:semaphore_get
if exist %1 (
timeout /t 1 >nul
goto semaphore_get
)
echo. > %1
goto :eof
:semaphore_release
rem Remove the lock file so others can write
del /q %1
goto :eof
:exiting
if not {%NOPAUSE%}=={1} pause
Changelog
v3.6 Tracking parallel instance numbers in main loop and throttling instance launches.
v3.5 supports _patch.nfo in PatchData or file_id.diz in game folder. If any of them found its content is printed and asked if user wants to continue.
IMDEBUG_FAKE_PATCHER var for debug mode - will rename a copy of timeout.exe as emulated patcher exe.
v3.4 Supports logging (LOG_ENABLED var) and logger tool with special name USXPM_tee.exe
Forcefully change dir to the one where script is in - can safely run as Administrator now (not recommended still)
v3.3 Support SIZE_{keyfilesize} in 1st column of _keyfile.txt
v3.2 Parallel processing free space estimation using extended freespace_req_file.
New special file USXPM_bc.exe (renamed GNU bc to avoid collisions with games which have main exe named bc.exe). Patching in parallel mode now only works if PatchData or game root folder has USXPM_bc.exe!
USXPM_PATCHER.bat creates USXPM_PATCHER_command_list.txt in %TEMP% with commands for selective manual xdelta reapplication.
v3.1b improved synchronization if patch has few xdeltas
v3.1 Debugged (it worked only when using start command to launch the script) and speeded up parallel application
v3.0 Parallel patch application, speed comparable to ISXPM GUI parallelizing patcher.
v2.2a Doesn't use xcopy /-I which is not implemented in WINE.
v2.2 Major WINE support. Correctly creates dynamic patchlist under WINE.
Uses md5sum.exe, sha1sum.exe for hash check under WINE because it doesn't have feature-complete certutil. Executes prepatch script only if there's something to patch,
unpacking ExternalDataFolder only if something was actually patched
v2.1a free space figure calc as max between single biggest file/all patched files and total added files. get_max subroutine.
v2.1 look for ExternalData beside custom PatchData
v2.0a improved DYNAMIC_PATCH_LST logic, set to 0 if explicit _patch.lst found
v2.0 supports reading freespace requirement file _freespace_req.txt in PatchData
CSV with columns
1 2 3 (opt) 4 (opt)
mod_size_max mod_size_total junked_size_total added_size_total
Can run multiple patchers, deleting temporary dynamic %RANDOM%_patch.lst from %TEMP%
v1.9 using smaller unarc.exe instead of bigger arc.exe as unpacker for ExternalData
%CLEANUP_AFTER% var to delete PatchData and script itself after successful patching.
v1.9a supports explicit _patch.lst inside PatchData to safeguard against leftover xdeltas
which otherwise would be included to dynamic patch list, rejected and reported as errors
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