Sunday, June 21, 2026

Manual application of ISXPM patches

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

  1. Unpack xdeltas in PatchData folder into temporary subfolder in %TEMP% (if patch was made with "Place patch-data inside a patch" option)
  2. 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!
  3. 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

  1. Create the .bat file (copy and paste the code below) 
  2. 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%.
  3. Identify xdelta file extension (patchext variable). Some files in PatchData e.g. those that are newly added  are not xdeltas!
  4. 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. 
  5. 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
echo USXPM_PATCHER v2.2 2026 Anton Shabunin https://ant-sh.blogspot.com
echo Part of UNIX-Style Xdelta Patch Maker (USXPM) suite
echo ==================================================================
rem Applies xdeltas from ISXPM-compatible PatchData folder
rem Formerly ISXPM_MANUAL.bat, now part of UNIX-Style Xdelta Patch Maker (USXPM) suite.
rem  set IMDEBUG=1

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
rem multi-thread 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 patcher_params=-p-5 -f
rem for xdelta3 set patcher_params=-d -s
set unarcexe_name=unarc.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 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 Will remove script itself, executables, lists, readme after successful patch application
    set CLEANUP_AFTER=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 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 _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
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

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
)

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 ============================ 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 (
        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 counting extension length
call :stringlength %patchext% length
echo Xdelta extension is %patchext%, length %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 - continuing
  goto :determine_patch_list
)
set mod_size_max=0
set mod_size_total=0
set junked_size_total=0
set added_size_total=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 {%IMDEBUG%}=={1} (
     echo A max ch is "%%A"
     echo B total ch is "%%B"
     echo C junked is "%%C"
     echo D added is "%%D"
   )
   set mod_size_max=%%A
   set mod_size_total=%%B
   set junked_size_total=%%C
   set added_size_total=%%D
)
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} (
  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 is %str1%, str2 is %str2%
if "%str1%" GTR "%str2%" (
    echo Free space %free_space% bytes OK for %size_to_check_for% bytes required
) 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%" (
  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%" (
  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 {%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
echo DYNAMIC_PATCH_LST is %DYNAMIC_PATCH_LST%
echo patch_lst is %patch_lst%, total_xdeltas is %total_xdeltas%

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
set /a n=0
set /a num_patched_files=0
if %total_xdeltas% 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%
    )
 )
 if not exist "%outdir%" md "%outdir%"
 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 (
         set /a num_patched_files+=1
         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 (
       echo DEBUG printing only - no actual files patched
       rem 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
            rem WINE doesn't support /-I and may ask if it is a folder or a file. Need to press F
            xcopy "!patchdir!\%%A" "!filename!" /T /-I /Y
          )
          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
          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 Avoid cluttering %TEMP%
 if {%DYNAMIC_PATCH_LST%}=={1} del /q %patch_lst%
)

 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% total xdeltas or files
   echo ERROR nothing was processed - exiting
   goto :exiting
) else (
 if {%num_patched_files%}=={0} (
   echo ERROR no xdeltas were applied - exiting
   goto :exiting
 )
)

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 exist "%ExternalDataFolder%\ExternalData.arc" (
  if defined arcexe (
     %arcexe% x -o+ "%ExternalDataFolder%\ExternalData.arc"
     rem %arcexe% x ExternalData\ExternalData.arc --overwrite=+
     if {%DELETE_FILES_FROM_PATCHDIR%}=={1} rmdir /q /s "%ExternalDataFolder%"
  ) 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
)
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 
  echo     OR
  echo check "!patchdir!" folder for old leftover xdeltas from previous patches
  echo and delete them.
  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
  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 {%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
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

:exiting
pause



Changelog

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