@@ -45,14 +45,14 @@ public static IEnumerable<string> GetTocableFiles(string path)
4545 private static List < string > GetFiles ( string basefolder , bool isLE2LE3 )
4646 {
4747 var res = new List < string > ( ) ;
48- string directoryName = Path . GetFileName ( Path . GetDirectoryName ( basefolder ) ) ;
48+ string directoryName = Path . GetFileName ( basefolder ) ;
4949 // Do not include the directory's existing PCConsoleTOC.bin
5050 res . AddRange ( GetTocableFiles ( basefolder ) . Except ( new [ ] { Path . Combine ( basefolder , "PCConsoleTOC.bin" ) } , StringComparer . InvariantCultureIgnoreCase ) ) ;
5151 DirectoryInfo folder = new DirectoryInfo ( basefolder ) ;
5252 var folders = folder . GetDirectories ( ) ;
5353 if ( folders . Length != 0 )
5454 {
55- if ( ! directoryName . Equals ( "BioGame" , StringComparison . InvariantCultureIgnoreCase ) )
55+ if ( ! folder . Name . Equals ( "BioGame" , StringComparison . InvariantCultureIgnoreCase ) )
5656 {
5757 //treat as dlc and include all folders.
5858 foreach ( DirectoryInfo f in folders )
@@ -63,10 +63,8 @@ private static List<string> GetFiles(string basefolder, bool isLE2LE3)
6363 //biogame, only do cookedpcconsole and movies.
6464 foreach ( DirectoryInfo f in folders )
6565 {
66- if ( f . Name == "CookedPCConsole" || f . Name == "Movies" )
66+ if ( f . Name == "CookedPCConsole" || f . Name == "Movies" || f . Name == "DLC" )
6767 res . AddRange ( GetFiles ( Path . Combine ( basefolder , f . Name ) , isLE2LE3 ) ) ;
68- else if ( isLE2LE3 && f . Name == "DLC" )
69- res . AddRange ( GetFiles ( Path . Combine ( basefolder , f . Name ) , isLE2LE3 ) ) ; // may need updated when we get LE1 DLC system up
7068 else if ( f . Name == "Content" )
7169 res . AddRange ( GetFiles ( Path . Combine ( basefolder , f . Name , "Packages" , "ISACT" ) , isLE2LE3 ) ) ;
7270
@@ -94,7 +92,7 @@ public static MemoryStream CreateTOCForDirectory(string directory, MEGame game)
9492 //Strip the non-relative path information
9593 string file0fullpath = files [ 0 ] ;
9694 int dlcFolderStartSubStrPos = file0fullpath . IndexOf ( "DLC_" , StringComparison . InvariantCultureIgnoreCase ) ;
97- if ( dlcFolderStartSubStrPos > 0 )
95+ if ( dlcFolderStartSubStrPos > 0 && game != MEGame . LE1 )
9896 {
9997 // DLC TOC
10098 files = files . Select ( x => x . Substring ( dlcFolderStartSubStrPos ) ) . ToList ( ) ;
@@ -134,88 +132,143 @@ public static MemoryStream CreateTOCForDirectory(string directory, MEGame game)
134132 /// </summary>
135133 /// <param name="filesystemInfo">list of filenames and sizes for the TOC</param>
136134 /// <returns>memorystream of TOC, null if list is empty</returns>
137- public static MemoryStream CreateTOCForEntries ( List < ( string filename , int size ) > filesystemInfo )
135+ public static MemoryStream CreateTOCForEntries ( List < ( string relativeFilename , int size ) > filesystemInfo )
138136 {
139137 if ( filesystemInfo . Count != 0 )
140138 {
141139 var tbf = new TOCBinFile ( ) ;
142140
143- // Todo: Update this someday so it lines up with the actual correct implementation
144- var hashBucket = new TOCBinFile . TOCHashTableEntry ( ) ;
145- tbf . HashBuckets . Add ( hashBucket ) ;
146- hashBucket . TOCEntries . AddRange ( filesystemInfo . Select ( x => new TOCBinFile . Entry
147- {
148- flags = 0 ,
149- name = x . filename ,
150- size = x . size
151- } ) ) ;
152-
153- return tbf . Save ( ) ;
154- }
141+ // Generate hashes for all names
142+ Dictionary < ( string relativeFileName , int size ) , uint > fullHashMap =
143+ new Dictionary < ( string relativeFileName , int size ) , uint > ( ) ; // Unbounded hash table size
155144
156- return null ;
157- /*
158- MemoryStream fs = MemoryManager.GetMemoryStream();
159-
160- fs.WriteInt32(TOCBinFile.TOCMagicNumber); // Endian check
161- fs.WriteInt32(0x0); // Media Data Count
162- fs.WriteInt32(0x1); // Hash Table Count
145+ foreach ( var f in filesystemInfo )
146+ {
147+ fullHashMap [ f ] = GetStringFullHash ( Path . GetFileName ( f . relativeFilename ) ) ;
148+ }
163149
164- // TOCHashTableEntry (Only have 1 entry)
165- fs.WriteInt32(0x8) ; // Offset of first entry from
166- fs.WriteInt32(filesystemInfo.Count); // Number of files in this table
150+ // Calculate optimal hash table size for performance
151+ var hashTableSize = filesystemInfo . Count ; // Initial size is 100% 1:1
152+ List < TOCBinFile . TOCHashTableEntry > hashBuckets ;
167153
168- for (int i = 0; i < filesystemInfo.Count; i++ )
154+ while ( true )
169155 {
156+ hashBuckets = new List < TOCBinFile . TOCHashTableEntry > ( ) ;
157+ for ( int i = 0 ; i < hashTableSize ; i ++ ) hashBuckets . Add ( new TOCBinFile . TOCHashTableEntry ( ) ) ;
170158
171- // TOCFileEntry - 4 Byte Aligned
172- (string file, int size) entry = filesystemInfo[i];
159+ //Populate the buckets with file entries
160+ foreach ( var hashPair in fullHashMap )
161+ {
162+ var bucketIdx = GetBoundedHashValue ( hashPair . Value , hashTableSize ) ;
163+ hashBuckets [ ( int ) bucketIdx ] . TOCEntries . Add ( new TOCBinFile . Entry ( )
164+ {
165+ flags = 0 ,
166+ name = hashPair . Key . relativeFileName ,
167+ size = hashPair . Key . size
168+ } ) ;
169+ }
173170
174- // Next Entry Offset
175- if (i == filesystemInfo.Count - 1) // Last entry has no next offset
176- fs.WriteAligned(BitConverter.GetBytes((ushort)0), 0, 2, 4);
177- else
178- fs.WriteAligned(BitConverter.GetBytes((ushort)entry.file.Length), 0, 2, 4);
179171
180- //nextEntryOffsetPos = fs.Position;
181- fs.WriteUInt16((ushort)(0x1D + entry.file.Length)); // Next entry start offset
172+ // Check fill rate. We must have over 75% fill rate or we will lower the hash table size by 25% and try again (down to 50% of file table size)
173+ var emptyHashBucketsCount = hashBuckets . Count ( x => x . TOCEntries . Count == 0 ) ;
182174
183- // Flags
184- fs.WriteUInt16(0);
175+ //Console.WriteLine($@"Hash fill rate: {100 - (emptyHashBucketsCount * 100.0f / hashTableSize)}%");
185176
186- // FileSize
187- if (!Path.GetFileName(entry.file).Equals("PCConsoleTOC.bin", StringComparison.InvariantCultureIgnoreCase))
177+ if ( emptyHashBucketsCount > hashTableSize / 4 )
188178 {
189- fs.WriteInt32(entry.size);
179+ int shrunkTableSize = Math . Max ( filesystemInfo . Count / 2 , hashTableSize - ( hashTableSize / 4 ) ) ;
180+ if ( shrunkTableSize == hashTableSize )
181+ {
182+ // WARNING: This will be suboptimal hash table size but we are going to use it anyways to prevent crash
183+ Console . WriteLine ( @"WARNING: Hash table fill rate is low; even at 50% of the file size" ) ;
184+ Console . WriteLine ( @"This TOC file will work, but is suboptimal." ) ;
185+ break ;
186+ }
187+
188+ // Update size
189+ hashTableSize = shrunkTableSize ;
190+ continue ;
190191 }
191192 else
192193 {
193- selfSizePosition = fs.Position; // Save self-position so we can rewrite our own file size at the end
194- fs.WriteInt32(0) ;
194+ // This is a satisfactory TOC
195+ break ;
195196 }
197+ }
196198
197- // SHA1 - Not used by games...
198- fs.WriteZeros(20 );
199- fs.WriteStringLatin1(entry.file); // Not present in hash table?
199+ tbf . HashBuckets = hashBuckets ;
200+ return tbf . Save ( ) ;
201+ }
200202
201- // Old method
202- //foreach (char c in file)
203- // fs.WriteByte((byte)c);
204- fs.WriteByte(0);
205- }
203+ return null ;
204+ }
206205
207- if (selfSizePosition >= 0)
208- {
209- // Write the size of our own TOC. This ensures TOC appears up to date when we try to update it later
210- // (important for DLC TOCs)
211- fs.Seek(selfSizePosition, SeekOrigin.Begin);
212- fs.WriteInt32((int)fs.Length);
213- }
206+ /// <summary>
207+ /// Gets the hash value of a string without bounding (UE3-hash)
208+ /// </summary>
209+ /// <param name="strToHash"></param>
210+ /// <returns></returns>
211+ private static uint GetStringFullHash ( string strToHash )
212+ {
213+ initCRCTable ( ) ;
214214
215- return fs;
215+ uint hash = 0 ;
216+ var upperCaseStr = strToHash . ToUpper ( ) ;
217+ for ( var i = 0 ; i < upperCaseStr . Length ; ++ i )
218+ {
219+ char upperChar = upperCaseStr [ i ] ;
220+ hash = ( ( hash >> 8 ) & 0x00FFFFFF ) ^ crcTable [ ( hash ^ ( ( byte ) upperChar ) ) & 0x000000FF ] ; // ASCII
221+ hash = ( ( hash >> 8 ) & 0x00FFFFFF ) ^ crcTable [ ( hash ) & 0x000000FF ] ; // This is for unicode as each character is two bytes.
216222 }
223+ return hash ;
224+ }
225+
226+ /// <summary>
227+ /// Gets the hash value of a string with bounding (UE3-hash)
228+ /// </summary>
229+ /// <param name="inputString"></param>
230+ /// <param name="hashTableSize"></param>
231+ /// <returns></returns>
232+ private static uint GetStringHashBounded ( string inputString , int hashTableSize )
233+ {
234+ return ( uint ) ( GetStringFullHash ( inputString ) % hashTableSize ) ;
235+ }
236+
237+ /// <summary>
238+ /// Applies the specified bound to the listed hash
239+ /// </summary>
240+ /// <param name="hash"></param>
241+ /// <param name="bound"></param>
242+ /// <returns></returns>
243+ private static uint GetBoundedHashValue ( uint hash , int bound )
244+ {
245+ return ( uint ) ( hash % bound ) ;
246+ }
247+
248+ private static uint [ ] crcTable ;
249+
250+ /// <summary>
251+ /// Polynomial for our CRCs
252+ /// </summary>
253+ private const uint CRC_POLYNOMIAL = 0x04C11DB7 ;
217254
218- return null;*/
255+ /// <summary>
256+ /// Initializes the CRC table which is used for calculating a hash
257+ /// </summary>
258+ private static void initCRCTable ( )
259+ {
260+ if ( crcTable != null ) return ;
261+ crcTable = new uint [ 256 ] ;
262+ // Table has 256 entries.
263+ for ( uint idx = 0 ; idx < 256 ; idx ++ )
264+ {
265+ // Generate CRCs based on the polynomial
266+ for ( uint crc = idx << 24 , bitIdx = 8 ; bitIdx != 0 ; bitIdx -- )
267+ {
268+ crc = ( ( crc & 0x80000000 ) == 0x80000000 ) ? ( crc << 1 ) ^ CRC_POLYNOMIAL : crc << 1 ;
269+ crcTable [ idx ] = crc ;
270+ }
271+ }
219272 }
220273 }
221274}
0 commit comments