GGPK Defragmenter
| require 'bindata' | |
| require 'benchmark' | |
| require 'win32/registry' | |
| require 'io/console' | |
| class GGPK | |
| class UTF16String < BinData::String | |
| def snapshot | |
| super.force_encoding('UTF-16LE') | |
| end | |
| end | |
| class RecordBase < BinData::Record | |
| endian :little | |
| uint32 :record_length | |
| string :tag, :length => 4 | |
| end | |
| class GrindingGearRecord < BinData::Record | |
| endian :little | |
| record_base :base | |
| uint32 :child_count, :value => lambda { children.length } | |
| array :children, :type => :uint64, :initial_length => :child_count | |
| end | |
| class DirectoryEntry < BinData::Record | |
| endian :little | |
| uint32 :timestamp | |
| uint64 :absolute_offset | |
| end | |
| class FileRecord < BinData::Record | |
| endian :little | |
| uint32 :name_length, :value => lambda { name.length } | |
| string :digest, :length => 32 # hash of the following data | |
| utf16_string :name, :read_length => lambda { name_length * 2 } | |
| def utf8_name | |
| name.encode('UTF-8').chomp("\0") | |
| end | |
| end | |
| class DirectoryRecord < BinData::Record | |
| endian :little | |
| uint32 :name_length, :value => lambda { name.length } | |
| uint32 :child_count, :value => lambda { children.length } | |
| string :digest, :length => 32 | |
| utf16_string :name, :read_length => lambda { name_length * 2 } | |
| array :children, :type => :directory_entry, :initial_length => :child_count | |
| def utf8_name | |
| name.encode('UTF-8').chomp("\0") | |
| end | |
| end | |
| class FreeRecord < BinData::Record | |
| endian :little | |
| uint64 :next_record | |
| end | |
| def initialize(file) | |
| @file = file | |
| @root = GrindingGearRecord.read(@file) | |
| @free_space = 0 | |
| raise "Invalid input file" unless @root.base.tag == "GGPK" | |
| end | |
| def traverse_children(path, children) | |
| children.each do |child| | |
| @file.pos = child | |
| base = RecordBase.read(@file) | |
| if base.tag == 'PDIR' | |
| dir = DirectoryRecord.read(@file) | |
| traverse_children(path + dir.utf8_name + '/', dir.children.map(&:absolute_offset)) | |
| elsif base.tag == 'FILE' | |
| file = FileRecord.read(@file) | |
| puts path + file.utf8_name | |
| elsif base.tag == 'FREE' | |
| @free_space += base.record_length - base.num_bytes | |
| else | |
| raise "Invalid tag #{base.tag}" | |
| end | |
| end | |
| end | |
| def list | |
| traverse_children('', @root.children) | |
| puts @free_space | |
| end | |
| def defragment(other) | |
| @dest = other | |
| dest_root = GrindingGearRecord.new | |
| dest_root.children = [0] * @root.children.size | |
| dest_root.base.tag = 'GGPK' | |
| dest_root.base.record_length = dest_root.num_bytes | |
| @dest.seek dest_root.num_bytes | |
| puts "This will likely take a while..." | |
| dest_root.children = copy_children(@root, @root.children.map do |child| | |
| entry = DirectoryEntry.new | |
| entry.absolute_offset = child | |
| entry | |
| end).map(&:absolute_offset) | |
| @dest.seek 0 | |
| dest_root.write(@dest) | |
| end | |
| def copy_children(root, children) | |
| children.map do |child| | |
| @file.pos = child.absolute_offset | |
| result = DirectoryEntry.new | |
| result.timestamp = child.timestamp | |
| base = RecordBase.read(@file) | |
| dest_base = RecordBase.new | |
| dest_base.tag = base.tag | |
| if base.tag == 'PDIR' | |
| dir = DirectoryRecord.read(@file) | |
| dest_dir = DirectoryRecord.new | |
| dest_dir.name = dir.name | |
| children = copy_children(dir, dir.children) | |
| dest_dir.children = children | |
| dest_dir.digest = dir.digest | |
| dest_base.record_length = dest_base.num_bytes + dest_dir.num_bytes | |
| result.absolute_offset = @dest.pos | |
| dest_base.write(@dest) | |
| dest_dir.write(@dest) | |
| elsif base.tag == 'FILE' | |
| file = FileRecord.read(@file) | |
| remaining = base.record_length - (base.num_bytes + file.num_bytes) | |
| dest_file = FileRecord.new | |
| dest_base.record_length = base.record_length | |
| dest_file.name = file.name | |
| dest_file.digest = file.digest | |
| result.absolute_offset = @dest.pos | |
| dest_base.write(@dest) | |
| dest_file.write(@dest) | |
| data = @file.read(remaining) | |
| @dest.write(data) | |
| elsif base.tag == 'FREE' | |
| dest_free = FreeRecord.new | |
| dest_free.next_record = 0 | |
| dest_base.record_length = dest_base.num_bytes + dest_free.num_bytes | |
| result.absolute_offset = @dest.pos | |
| dest_base.write(@dest) | |
| dest_free.write(@dest) | |
| else | |
| raise "Invalid tag #{base.tag}" | |
| end | |
| result | |
| end | |
| end | |
| end | |
| def finish(msg) | |
| puts msg, "\nPress a key to close this application." | |
| STDIN.getch | |
| exit | |
| end | |
| input = ARGV.first | |
| unless input | |
| begin | |
| Win32::Registry::HKEY_CURRENT_USER.open('Software\GrindingGearGames\Path of Exile') do |reg| | |
| input = File.join(reg['InstallLocation'].to_s, 'Content.ggpk') | |
| input = nil unless File.exists?(input) | |
| end | |
| rescue Win32::Registry::Error | |
| end | |
| end | |
| finish "No GGPK given and no Path of Exile installation detected. Please drag and drop the GGPK to defragment on this executable." unless input | |
| finish "The given GGPK file '#{input}' doesn't exist" unless File.exists?(input) | |
| puts "Defragmenting #{input}" | |
| new = input + '.new' | |
| begin | |
| fin = File.open(input, 'r+b') | |
| rescue SystemCallError | |
| finish "The GGPK appears to be in use. Please close Path of Exile." | |
| end | |
| old_size = fin.size | |
| new_size = nil | |
| time = Benchmark.realtime do | |
| begin | |
| File.open(new, 'w+b') do |fnew| | |
| fnew.seek 0 | |
| GGPK.new(fin).defragment(fnew) | |
| new_size = fnew.size | |
| end | |
| fin.close | |
| rescue Exception | |
| File.delete(new) | |
| raise | |
| end | |
| File.delete(input) | |
| File.rename(new, input) | |
| end | |
| puts "Elapsed time: #{(time / 60.0).round(2)} minute(s)" | |
| puts "File reduction: #{((old_size - new_size) / 1024.0 / 1024.0).round(2)} MB(s)" | |
| finish "Successfully defragmented the GGPK." |
This comment has been minimized.
This comment has been minimized.
|
very nice! |
This comment has been minimized.
This comment has been minimized.
|
nasıl yükleniyor bilgisayara .yardımcı olurmusunuz |
This comment has been minimized.
This comment has been minimized.
|
Is this still useful/needed? |
This comment has been minimized.
This comment has been minimized.
|
@Reithan Not if you use Steam, but I don't know if this is needed or works with the standalone client still. |
This comment has been minimized.
This comment has been minimized.
|
Still works and is still needed with the stand-alone client. After the recent update, perhaps the third since I last ran it, I ran ggpk_defragment and it reduced content.ggpk's file size by 9720.15MB. |
This comment has been minimized.
This comment has been minimized.
|
For now with 3.11.2 Patch it has stopped working, throws EOF Error.
|
This comment has been minimized.
This comment has been minimized.
https://www.pathofexile.com/forum/view-thread/2934311 diff --git a/ggpk_compact.rb b/ggpk_compact.rb
index 9e6e1a0..d0a9afe 100755
--- a/ggpk_compact.rb
+++ b/ggpk_compact.rb
@@ -23,8 +23,8 @@ class GGPK
class GrindingGearRecord < BinData::Record
endian :little
record_base :base
- uint32 :child_count, :value => lambda { children.length }
- array :children, :type => :uint64, :initial_length => :child_count
+ uint32 :version, :value => 3
+ array :children, :type => :uint64, :initial_length => 2
end
class DirectoryEntry < BinData::Record |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Nice!