source: OniSplit/InstanceFileWriter.cs@ 1133

Last change on this file since 1133 was 1114, checked in by iritscen, 5 years ago

Adding OniSplit source code (v0.9.99.0). Many thanks to Neo for all his work over the years.

File size: 46.2 KB
RevLine 
[1114]1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.IO;
5using System.Text;
6using Oni.Collections;
7using Oni.Metadata;
8
9namespace Oni
10{
11 internal sealed class InstanceFileWriter
12 {
13 #region Private data
14 private static readonly byte[] padding = new byte[512];
15 private static byte[] copyBuffer1 = new byte[32768];
16 private static byte[] copyBuffer2 = new byte[32768];
17 private StreamCache streamCache;
18 private readonly bool bigEndian;
19 private readonly Dictionary<string, int> namedInstancedIdMap;
20 private readonly FileHeader header;
21 private readonly List<DescriptorTableEntry> descriptorTable;
22 private NameDescriptorTable nameIndex;
23 private TemplateDescriptorTable templateTable;
24 private NameTable nameTable;
25 private readonly Dictionary<InstanceDescriptor, InstanceDescriptor> sharedMap;
26 private readonly Dictionary<InstanceFile, int[]> linkMaps;
27 private readonly Dictionary<InstanceFile, Dictionary<int, int>> rawOffsetMaps;
28 private readonly Dictionary<InstanceFile, Dictionary<int, int>> sepOffsetMaps;
29 private int rawOffset;
30 private int sepOffset;
31 private List<BinaryPartEntry> rawParts;
32 private List<BinaryPartEntry> sepParts;
33 #endregion
34
35 #region private class FileHeader
36
37 private class FileHeader
38 {
39 public const int Size = 64;
40
41 public long TemplateChecksum;
42 public int Version;
43 public int InstanceCount;
44 public int NameCount;
45 public int TemplateCount;
46 public int DataTableOffset;
47 public int DataTableSize;
48 public int NameTableOffset;
49 public int NameTableSize;
50 public int RawTableOffset;
51 public int RawTableSize;
52
53 public void Write(BinaryWriter writer)
54 {
55 writer.Write(TemplateChecksum);
56 writer.Write(Version);
57 writer.Write(InstanceFileHeader.Signature);
58 writer.Write(InstanceCount);
59 writer.Write(NameCount);
60 writer.Write(TemplateCount);
61 writer.Write(DataTableOffset);
62 writer.Write(DataTableSize);
63 writer.Write(NameTableOffset);
64 writer.Write(NameTableSize);
65 writer.Write(RawTableOffset);
66 writer.Write(RawTableSize);
67 writer.Write(0);
68 writer.Write(0);
69 }
70 }
71
72 #endregion
73 #region private class DescriptorTableEntry
74
75 private class DescriptorTableEntry
76 {
77 public const int Size = 20;
78
79 public readonly InstanceDescriptor SourceDescriptor;
80 public readonly int Id;
81 public int DataOffset;
82 public int NameOffset;
83 public int DataSize;
84 public bool AnimationPositionPointHack;
85
86 public DescriptorTableEntry(int id, InstanceDescriptor descriptor)
87 {
88 Id = id;
89 SourceDescriptor = descriptor;
90 }
91
92 public bool HasName => SourceDescriptor.HasName;
93 public string Name => SourceDescriptor.FullName;
94 public TemplateTag Code => SourceDescriptor.Template.Tag;
95 public InstanceFile SourceFile => SourceDescriptor.File;
96
97 public void Write(BinaryWriter writer, bool shared)
98 {
99 writer.Write((int)Code);
100 writer.Write(DataOffset);
101 writer.Write(NameOffset);
102 writer.Write(DataSize);
103
104 var flags = InstanceDescriptorFlags.None;
105
106 if (!SourceDescriptor.HasName)
107 flags |= InstanceDescriptorFlags.Private;
108
109 if (DataOffset == 0)
110 flags |= InstanceDescriptorFlags.Placeholder;
111
112 if (shared)
113 flags |= InstanceDescriptorFlags.Shared;
114
115 writer.Write((int)flags);
116 }
117 }
118
119 #endregion
120 #region private class NameDescriptorTable
121
122 private class NameDescriptorTable
123 {
124 private List<Entry> entries;
125
126 #region private class Entry
127
128 private class Entry : IComparable<Entry>
129 {
130 public const int Size = 8;
131
132 public int InstanceNumber;
133 public string Name;
134
135 public void Write(BinaryWriter writer)
136 {
137 writer.Write(InstanceNumber);
138 writer.Write(0);
139 }
140
141 #region IComparable<NameIndexEntry> Members
142
143 int IComparable<Entry>.CompareTo(Entry other)
144 {
145 //
146 // Note: Oni is case sensitive so we need to sort names accordingly.
147 //
148
149 return string.CompareOrdinal(Name, other.Name);
150 }
151
152 #endregion
153 }
154
155 #endregion
156
157 public static NameDescriptorTable CreateFromDescriptors(List<DescriptorTableEntry> descriptorTable)
158 {
159 NameDescriptorTable nameIndex = new NameDescriptorTable();
160
161 nameIndex.entries = new List<Entry>();
162
163 for (int i = 0; i < descriptorTable.Count; i++)
164 {
165 DescriptorTableEntry descriptor = descriptorTable[i];
166
167 if (descriptor.HasName)
168 {
169 Entry entry = new Entry();
170 entry.Name = descriptor.Name;
171 entry.InstanceNumber = i;
172 nameIndex.entries.Add(entry);
173 }
174 }
175
176 nameIndex.entries.Sort();
177
178 return nameIndex;
179 }
180
181 public int Count => entries.Count;
182 public int Size => entries.Count * Entry.Size;
183
184 public void Write(BinaryWriter writer)
185 {
186 foreach (Entry entry in entries)
187 entry.Write(writer);
188 }
189 }
190
191 #endregion
192 #region private class TemplateDescriptorTable
193
194 private class TemplateDescriptorTable
195 {
196 private List<Entry> entries;
197
198 #region private class Entry
199
200 private class Entry : IComparable<Entry>
201 {
202 public const int Size = 16;
203
204 public long Checksum;
205 public TemplateTag Code;
206 public int Count;
207
208 public void Write(BinaryWriter writer)
209 {
210 writer.Write(Checksum);
211 writer.Write((int)Code);
212 writer.Write(Count);
213 }
214
215 int IComparable<Entry>.CompareTo(Entry other) => Code.CompareTo(other.Code);
216 }
217
218 #endregion
219
220 public static TemplateDescriptorTable CreateFromDescriptors(InstanceMetadata metadata, List<DescriptorTableEntry> descriptorTable)
221 {
222 Dictionary<TemplateTag, int> templateCount = new Dictionary<TemplateTag, int>();
223
224 foreach (DescriptorTableEntry entry in descriptorTable)
225 {
226 int count;
227 templateCount.TryGetValue(entry.Code, out count);
228 templateCount[entry.Code] = count + 1;
229 }
230
231 TemplateDescriptorTable templateTable = new TemplateDescriptorTable();
232 templateTable.entries = new List<Entry>(templateCount.Count);
233
234 foreach (KeyValuePair<TemplateTag, int> pair in templateCount)
235 {
236 Entry entry = new Entry();
237 entry.Checksum = metadata.GetTemplate(pair.Key).Checksum;
238 entry.Code = pair.Key;
239 entry.Count = pair.Value;
240 templateTable.entries.Add(entry);
241 }
242
243 templateTable.entries.Sort();
244
245 return templateTable;
246 }
247
248 public int Count => entries.Count;
249
250 public int Size => entries.Count * Entry.Size;
251
252 public void Write(BinaryWriter writer)
253 {
254 foreach (Entry entry in entries)
255 entry.Write(writer);
256 }
257 }
258
259 #endregion
260 #region private class NameTable
261
262 private class NameTable
263 {
264 private List<string> names;
265 private int size;
266
267 public static NameTable CreateFromDescriptors(List<DescriptorTableEntry> descriptors)
268 {
269 NameTable nameTable = new NameTable();
270
271 nameTable.names = new List<string>();
272
273 int nameTableSize = 0;
274
275 foreach (DescriptorTableEntry descriptor in descriptors)
276 {
277 if (!descriptor.HasName)
278 continue;
279
280 string name = descriptor.Name;
281
282 nameTable.names.Add(name);
283 descriptor.NameOffset = nameTableSize;
284 nameTableSize += name.Length + 1;
285
286 if (name.Length > 63)
287 Console.WriteLine("Warning: name '{0}' too long.", name);
288 }
289
290 nameTable.size = nameTableSize;
291
292 return nameTable;
293 }
294
295 public int Size => size;
296
297 public void Write(BinaryWriter writer)
298 {
299 byte[] copyBuffer = new byte[256];
300
301 foreach (string name in names)
302 {
303 int length = Encoding.UTF8.GetBytes(name, 0, name.Length, copyBuffer, 0);
304 copyBuffer[length] = 0;
305 writer.Write(copyBuffer, 0, length + 1);
306 }
307 }
308 }
309
310 #endregion
311 #region private class BinaryPartEntry
312
313 private class BinaryPartEntry : IComparable<BinaryPartEntry>
314 {
315 public readonly int SourceOffset;
316 public readonly string SourceFile;
317 public readonly int DestinationOffset;
318 public readonly int Size;
319 public readonly BinaryPartField Field;
320
321 public BinaryPartEntry(string sourceFile, int sourceOffset, int size, int destinationOffset, Field field)
322 {
323 SourceFile = sourceFile;
324 SourceOffset = sourceOffset;
325 Size = size;
326 DestinationOffset = destinationOffset;
327 Field = (BinaryPartField)field;
328 }
329
330 #region IComparable<BinaryPartEntry> Members
331
332 int IComparable<BinaryPartEntry>.CompareTo(BinaryPartEntry other)
333 {
334 //
335 // Sort the binary parts by destination offset in an attempt to streamline the write IO
336 //
337
338 return DestinationOffset.CompareTo(other.DestinationOffset);
339 }
340
341 #endregion
342 }
343
344 #endregion
345 #region private class ChecksumStream
346
347 private class ChecksumStream : Stream
348 {
349 private int checksum;
350 private int position;
351
352 public int Checksum => checksum;
353
354 public override bool CanRead => false;
355 public override bool CanSeek => false;
356 public override bool CanWrite => true;
357
358 public override void Flush()
359 {
360 }
361
362 public override long Length => position;
363
364 public override long Position
365 {
366 get
367 {
368 return position;
369 }
370 set
371 {
372 throw new NotSupportedException();
373 }
374 }
375
376 public override int Read(byte[] buffer, int offset, int count)
377 {
378 throw new NotSupportedException();
379 }
380
381 public override long Seek(long offset, SeekOrigin origin)
382 {
383 throw new NotSupportedException();
384 }
385
386 public override void SetLength(long value)
387 {
388 throw new NotSupportedException();
389 }
390
391 public override void Write(byte[] buffer, int offset, int count)
392 {
393 for (int i = offset; i < offset + count; i++)
394 checksum += buffer[i] ^ (i + position);
395
396 position += count;
397 }
398 }
399
400 #endregion
401 #region private class StreamCache
402
403 private class StreamCache : IDisposable
404 {
405 private const int maxCacheSize = 32;
406 private Dictionary<string, CacheEntry> cacheEntries = new Dictionary<string, CacheEntry>();
407
408 private class CacheEntry
409 {
410 public BinaryReader Stream;
411 public long LastTimeUsed;
412 }
413
414 public BinaryReader GetReader(InstanceDescriptor descriptor)
415 {
416 CacheEntry entry;
417
418 if (!cacheEntries.TryGetValue(descriptor.FilePath, out entry))
419 entry = OpenStream(descriptor);
420
421 entry.LastTimeUsed = DateTime.Now.Ticks;
422 entry.Stream.Position = descriptor.DataOffset;
423
424 return entry.Stream;
425 }
426
427 private CacheEntry OpenStream(InstanceDescriptor descriptor)
428 {
429 CacheEntry oldestEntry = null;
430 string oldestDescriptor = null;
431
432 if (cacheEntries.Count >= maxCacheSize)
433 {
434 foreach (KeyValuePair<string, CacheEntry> pair in cacheEntries)
435 {
436 if (oldestEntry == null || pair.Value.LastTimeUsed < oldestEntry.LastTimeUsed)
437 {
438 oldestDescriptor = pair.Key;
439 oldestEntry = pair.Value;
440 }
441 }
442 }
443
444 if (oldestEntry == null)
445 {
446 oldestEntry = new CacheEntry();
447 }
448 else
449 {
450 oldestEntry.Stream.Dispose();
451 cacheEntries.Remove(oldestDescriptor);
452 }
453
454 oldestEntry.Stream = new BinaryReader(descriptor.FilePath);
455 cacheEntries.Add(descriptor.FilePath, oldestEntry);
456
457 return oldestEntry;
458 }
459
460 public void Dispose()
461 {
462 foreach (CacheEntry entry in cacheEntries.Values)
463 entry.Stream.Dispose();
464 }
465 }
466
467 #endregion
468
469 public static InstanceFileWriter CreateV31(long templateChecksum, bool bigEndian)
470 {
471 return new InstanceFileWriter(templateChecksum, InstanceFileHeader.Version31, bigEndian);
472 }
473
474 public static InstanceFileWriter CreateV32(List<InstanceDescriptor> descriptors)
475 {
476 long templateChecksum;
477
478 if (descriptors.Exists(x => x.Template.Tag == TemplateTag.SNDD && x.IsMacFile))
479 templateChecksum = InstanceFileHeader.OniMacTemplateChecksum;
480 else
481 templateChecksum = InstanceFileHeader.OniPCTemplateChecksum;
482
483 var writer = new InstanceFileWriter(templateChecksum, InstanceFileHeader.Version32, false);
484 writer.AddDescriptors(descriptors, false);
485 return writer;
486 }
487
488 private InstanceFileWriter(long templateChecksum, int version, bool bigEndian)
489 {
490 if (templateChecksum != InstanceFileHeader.OniPCTemplateChecksum
491 && templateChecksum != InstanceFileHeader.OniMacTemplateChecksum
492 && templateChecksum != 0)
493 {
494 throw new ArgumentException("Unknown template checksum", "templateChecksum");
495 }
496
497 this.bigEndian = bigEndian;
498
499 header = new FileHeader
500 {
501 TemplateChecksum = templateChecksum,
502 Version = version
503 };
504
505 descriptorTable = new List<DescriptorTableEntry>();
506 namedInstancedIdMap = new Dictionary<string, int>();
507
508 linkMaps = new Dictionary<InstanceFile, int[]>();
509 rawOffsetMaps = new Dictionary<InstanceFile, Dictionary<int, int>>();
510 sepOffsetMaps = new Dictionary<InstanceFile, Dictionary<int, int>>();
511
512 sharedMap = new Dictionary<InstanceDescriptor, InstanceDescriptor>();
513 }
514
515 public void AddDescriptors(List<InstanceDescriptor> descriptors, bool removeDuplicates)
516 {
517 if (removeDuplicates)
518 {
519 Console.WriteLine("Removing duplicates");
520
521 using (streamCache = new StreamCache())
522 descriptors = RemoveDuplicates(descriptors);
523 }
524
525 //
526 // Initialize LinkMap table of each source file.
527 //
528
529 var inputFiles = new Set<InstanceFile>();
530
531 foreach (var descriptor in descriptors)
532 inputFiles.Add(descriptor.File);
533
534 foreach (var inputFile in inputFiles)
535 {
536 linkMaps[inputFile] = new int[inputFile.Descriptors.Count];
537 rawOffsetMaps[inputFile] = new Dictionary<int, int>();
538 sepOffsetMaps[inputFile] = new Dictionary<int, int>();
539 }
540
541 foreach (var descriptor in descriptors)
542 {
543 AddDescriptor(descriptor);
544 }
545
546 CreateHeader();
547 }
548
549 private void AddDescriptor(InstanceDescriptor descriptor)
550 {
551 //
552 // SNDD instances are special because they are different between PC
553 // and Mac/PC Demo versions so we need to check if the instance type matches the
554 // output file type. If the file type wasn't specified then we will set it when
555 // the first SNDD instance is seen.
556 //
557
558 if (descriptor.Template.Tag == TemplateTag.SNDD)
559 {
560 if (header.TemplateChecksum == 0)
561 {
562 header.TemplateChecksum = descriptor.TemplateChecksum;
563 }
564 else if (header.TemplateChecksum != descriptor.TemplateChecksum)
565 {
566 if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
567 throw new NotSupportedException(string.Format("File {0} cannot be imported due to conflicting template checksums", descriptor.FilePath));
568 }
569 }
570
571 //
572 // Create a new id for this descriptor and remember it and the old one
573 // in the LinkMap table of the source file.
574 //
575
576 int id = MakeInstanceId(descriptorTable.Count);
577
578 linkMaps[descriptor.File][descriptor.Index] = id;
579
580 //
581 // If the descriptor has a name we will need to know later what is the new id
582 // for its name.
583 //
584
585 if (descriptor.HasName)
586 namedInstancedIdMap[descriptor.FullName] = id;
587
588 //
589 // Create and add new table entry for this descriptor.
590 //
591
592 var entry = new DescriptorTableEntry(id, descriptor);
593
594 if (!descriptor.IsPlaceholder)
595 {
596 //
597 // .oni files have only one non empty named descriptor. The rest are
598 // forced to be empty and their contents stored in separate .oni files.
599 //
600
601 if (!IsV32
602 || !descriptor.HasName
603 || descriptorTable.Count == 0
604 || descriptorTable[0].SourceDescriptor == descriptor)
605 {
606 int dataSize = descriptor.DataSize;
607
608 if (descriptor.Template.Tag == TemplateTag.SNDD
609 && header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum
610 && descriptor.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
611 {
612 //
613 // HACK: when converting SNDD instances from PC Demo to PC Retail the resulting
614 // data size differs from the original.
615 //
616
617 dataSize = 0x60;
618 }
619 else if (descriptor.Template.Tag == TemplateTag.AKDA)
620 {
621 dataSize = 0x20;
622 }
623
624 entry.DataSize = dataSize;
625 entry.DataOffset = header.DataTableSize + 8;
626
627 header.DataTableSize += entry.DataSize;
628 }
629 }
630
631 descriptorTable.Add(entry);
632 }
633
634 private void CreateHeader()
635 {
636 if (header.TemplateChecksum == 0)
637 throw new InvalidOperationException("Target file format was not specified and cannot be autodetected.");
638
639 header.InstanceCount = descriptorTable.Count;
640
641 int offset = FileHeader.Size + descriptorTable.Count * DescriptorTableEntry.Size;
642
643 if (IsV31)
644 {
645 nameIndex = NameDescriptorTable.CreateFromDescriptors(descriptorTable);
646
647 header.NameCount = nameIndex.Count;
648 offset += nameIndex.Size;
649
650 templateTable = TemplateDescriptorTable.CreateFromDescriptors(
651 InstanceMetadata.GetMetadata(header.TemplateChecksum),
652 descriptorTable);
653
654 header.TemplateCount = templateTable.Count;
655 offset += templateTable.Size;
656
657 header.DataTableOffset = Utils.Align32(offset);
658
659 nameTable = NameTable.CreateFromDescriptors(descriptorTable);
660
661 header.NameTableSize = nameTable.Size;
662 header.NameTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize);
663 }
664 else
665 {
666 //
667 // .oni files do not need the name index and the template table.
668 // They consume space, complicate things and the information
669 // contained in them can be recreated anyway.
670 //
671
672 nameTable = NameTable.CreateFromDescriptors(descriptorTable);
673
674 header.NameTableSize = nameTable.Size;
675 header.NameTableOffset = Utils.Align32(offset);
676 header.DataTableOffset = Utils.Align32(header.NameTableOffset + nameTable.Size);
677 header.RawTableOffset = Utils.Align32(header.DataTableOffset + header.DataTableSize);
678 }
679 }
680
681 public void Write(string filePath)
682 {
683 string outputDirPath = Path.GetDirectoryName(filePath);
684
685 Directory.CreateDirectory(outputDirPath);
686
687 int fileId = IsV31 ? MakeFileId(filePath) : 0;
688
689 using (streamCache = new StreamCache())
690 using (var outputStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, 65536))
691 using (var writer = new BinaryWriter(outputStream))
692 {
693 outputStream.Position = FileHeader.Size;
694
695 foreach (DescriptorTableEntry entry in descriptorTable)
696 {
697 entry.Write(writer, sharedMap.ContainsKey(entry.SourceDescriptor));
698 }
699
700 if (IsV31)
701 {
702 nameIndex.Write(writer);
703 templateTable.Write(writer);
704 }
705 else
706 {
707 //
708 // For .oni files write the name table before the data table
709 // for better reading performance at import time.
710 //
711
712 writer.Position = header.NameTableOffset;
713 nameTable.Write(writer);
714 }
715
716 WriteDataTable(writer, fileId);
717
718 if (IsV31)
719 {
720 writer.Position = header.NameTableOffset;
721 nameTable.Write(writer);
722 }
723
724 WriteBinaryParts(writer, filePath);
725
726 if (IsV32 && outputStream.Length > header.RawTableOffset)
727 {
728 //
729 // The header was created with a RawTable size of 0 because
730 // we don't know the size in advance. Fix that now.
731 //
732
733 header.RawTableSize = (int)outputStream.Length - header.RawTableOffset;
734 }
735
736 outputStream.Position = 0;
737 header.Write(writer);
738 }
739 }
740
741 private void WriteDataTable(BinaryWriter writer, int fileId)
742 {
743 writer.Position = header.DataTableOffset;
744
745 //
746 // Raw and sep parts will be added as they are found. The initial offset
747 // is 32 because a 0 offset means "NULL".
748 //
749
750 rawOffset = 32;
751 rawParts = new List<BinaryPartEntry>();
752
753 sepOffset = 32;
754 sepParts = new List<BinaryPartEntry>();
755
756 var entries = descriptorTable.ToArray();
757 Array.Sort(entries, (x, y) => x.DataOffset.CompareTo(y.DataOffset));
758
759 foreach (var entry in entries)
760 {
761 if (entry.DataSize == 0)
762 continue;
763
764 int padSize = header.DataTableOffset + entry.DataOffset - 8 - writer.Position;
765
766 if (padSize <= 512)
767 writer.Write(padding, 0, padSize);
768 else
769 writer.Position = header.DataTableOffset + entry.DataOffset - 8;
770
771 writer.Write(entry.Id);
772 writer.Write(fileId);
773
774 var template = entry.SourceDescriptor.Template;
775
776 if (template.Tag == TemplateTag.SNDD
777 && entry.SourceDescriptor.File.Header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum
778 && header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum)
779 {
780 //
781 // Special case to convert PC Demo SNDD files to PC Retail SNDD files.
782 //
783
784 ConvertSNDDHack(entry, writer);
785 }
786 else
787 {
788 template.Type.Copy(streamCache.GetReader(entry.SourceDescriptor), writer, state =>
789 {
790 if (state.Type == MetaType.RawOffset)
791 RemapRawOffset(entry, state);
792 else if (state.Type == MetaType.SepOffset)
793 RemapSepOffset(entry, state);
794 else if (state.Type is MetaPointer)
795 RemapLinkId(entry, state);
796 });
797
798 if (entry.Code == TemplateTag.TXMP)
799 {
800 //
801 // HACK: All .oni files use the PC format except the SNDD ones. Most
802 // differences between PC and Mac formats are handled by the metadata
803 // but the TXMP is special because the raw/sep offset field is at a
804 // different offset.
805 //
806
807 ConvertTXMPHack(entry, writer.BaseStream);
808 }
809 }
810 }
811 }
812
813 private void ConvertSNDDHack(DescriptorTableEntry entry, BinaryWriter writer)
814 {
815 var reader = streamCache.GetReader(entry.SourceDescriptor);
816
817 int flags = reader.ReadInt32();
818 int duration = reader.ReadInt32();
819 int dataSize = reader.ReadInt32();
820 int dataOffset = reader.ReadInt32();
821
822 int channelCount = (flags == 3) ? 2 : 1;
823
824 writer.Write(8);
825 writer.WriteInt16(2);
826 writer.WriteInt16(channelCount);
827 writer.Write(22050);
828 writer.Write(11155);
829 writer.WriteInt16(512);
830 writer.WriteInt16(4);
831 writer.WriteInt16(32);
832 writer.Write(new byte[] {
833 0xf4, 0x03, 0x07, 0x00, 0x00, 0x01, 0x00, 0x00,
834 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00,
835 0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, 0x00,
836 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff
837 });
838
839 writer.Write((short)duration);
840 writer.Write(dataSize);
841 writer.Write(RemapRawOffsetCore(entry, dataOffset, null));
842 }
843
844 private void ConvertTXMPHack(DescriptorTableEntry entry, Stream stream)
845 {
846 stream.Position = header.DataTableOffset + entry.DataOffset + 0x80;
847 stream.Read(copyBuffer1, 0, 28);
848
849 if (header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum)
850 {
851 //
852 // Swap Bytes is always set for PC files.
853 //
854
855 copyBuffer1[1] |= 0x10;
856 }
857 else if (IsV31 && header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
858 {
859 //
860 // Swap Bytes if always set for MacPPC files except if the format is RGBA
861 // which requires no conversion.
862 //
863
864 if (bigEndian && copyBuffer1[8] == (byte)Motoko.TextureFormat.RGBA)
865 copyBuffer1[1] &= 0xef;
866 else
867 copyBuffer1[1] |= 0x10;
868 }
869
870 if (entry.SourceDescriptor.TemplateChecksum != header.TemplateChecksum)
871 {
872 //
873 // Swap the 0x94 and 0x98 fields to convert between Mac and PC TXMPs
874 //
875
876 for (int i = 20; i < 24; i++)
877 {
878 byte b = copyBuffer1[i];
879 copyBuffer1[i] = copyBuffer1[i + 4];
880 copyBuffer1[i + 4] = b;
881 }
882 }
883
884 stream.Position = header.DataTableOffset + entry.DataOffset + 0x80;
885 stream.Write(copyBuffer1, 0, 28);
886 }
887
888 private bool ZeroTRAMPositionPointsHack(DescriptorTableEntry entry, CopyVisitor state)
889 {
890 if (entry.Code != TemplateTag.TRAM)
891 return false;
892
893 int offset = state.GetInt32();
894
895 if (state.Position == 0x04)
896 {
897 entry.AnimationPositionPointHack = (offset == 0);
898 }
899 else if (state.Position == 0x28 && entry.AnimationPositionPointHack)
900 {
901 if (offset != 0)
902 {
903 InstanceFile input = entry.SourceFile;
904 int size = input.GetRawPartSize(offset);
905 offset = AllocateRawPart(null, 0, size, null);
906 state.SetInt32(offset);
907 }
908
909 return true;
910 }
911
912 return false;
913 }
914
915 private void RemapRawOffset(DescriptorTableEntry entry, CopyVisitor state)
916 {
917 if (ZeroTRAMPositionPointsHack(entry, state))
918 return;
919
920 state.SetInt32(RemapRawOffsetCore(entry, state.GetInt32(), state.Field));
921 }
922
923 private int RemapRawOffsetCore(DescriptorTableEntry entry, int oldOffset, Field field)
924 {
925 if (oldOffset == 0)
926 return 0;
927
928 InstanceFile input = entry.SourceFile;
929 Dictionary<int, int> rawOffsetMap = rawOffsetMaps[input];
930 int newOffset;
931
932 if (!rawOffsetMap.TryGetValue(oldOffset, out newOffset))
933 {
934 int size = input.GetRawPartSize(oldOffset);
935
936 //
937 // .oni files are always in PC format (except SNDD files) so when importing
938 // to a Mac file we need to allocate some binary parts in the sep file
939 // instead of the raw file.
940 //
941
942 if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum
943 && (entry.Code == TemplateTag.TXMP
944 || entry.Code == TemplateTag.OSBD
945 || entry.Code == TemplateTag.BINA))
946 {
947 newOffset = AllocateSepPart(input.RawFilePath, oldOffset + input.Header.RawTableOffset, size, null);
948 }
949 else
950 {
951 newOffset = AllocateRawPart(input.RawFilePath, oldOffset + input.Header.RawTableOffset, size, field);
952 }
953
954 rawOffsetMap[oldOffset] = newOffset;
955 }
956
957 return newOffset;
958 }
959
960 private void RemapSepOffset(DescriptorTableEntry entry, CopyVisitor state)
961 {
962 int oldOffset = state.GetInt32();
963
964 if (oldOffset == 0)
965 return;
966
967 InstanceFile input = entry.SourceFile;
968 Dictionary<int, int> sepOffsetMap = sepOffsetMaps[input];
969 int newOffset;
970
971 if (!sepOffsetMap.TryGetValue(oldOffset, out newOffset))
972 {
973 int size = input.GetSepPartSize(oldOffset);
974
975 //
976 // If we're writing a PC file then there is no sep file, everything gets allocated
977 // in the raw file
978 //
979
980 if (header.TemplateChecksum == InstanceFileHeader.OniPCTemplateChecksum)
981 newOffset = AllocateRawPart(input.SepFilePath, oldOffset, size, null);
982 else
983 newOffset = AllocateSepPart(input.SepFilePath, oldOffset, size, null);
984
985 sepOffsetMap[oldOffset] = newOffset;
986 }
987
988 state.SetInt32(newOffset);
989 }
990
991 private int AllocateRawPart(string sourceFile, int sourceOffset, int size, Field field)
992 {
993 var entry = new BinaryPartEntry(sourceFile, sourceOffset, size, rawOffset, field);
994 rawOffset = Utils.Align32(rawOffset + size);
995 rawParts.Add(entry);
996 return entry.DestinationOffset;
997 }
998
999 private int AllocateSepPart(string sourceFile, int sourceOffset, int size, Field field)
1000 {
1001 var entry = new BinaryPartEntry(sourceFile, sourceOffset, size, sepOffset, field);
1002 sepOffset = Utils.Align32(sepOffset + size);
1003 sepParts.Add(entry);
1004 return entry.DestinationOffset;
1005 }
1006
1007 private void RemapLinkId(DescriptorTableEntry entry, CopyVisitor state)
1008 {
1009 int oldId = state.GetInt32();
1010
1011 if (oldId != 0)
1012 {
1013 int newId = RemapLinkIdCore(entry.SourceDescriptor, oldId);
1014 state.SetInt32(newId);
1015 }
1016 }
1017
1018 private int RemapLinkIdCore(InstanceDescriptor descriptor, int id)
1019 {
1020 var file = descriptor.File;
1021
1022 if (IsV31)
1023 {
1024 InstanceDescriptor oldDescriptor = file.GetDescriptor(id);
1025 InstanceDescriptor newDescriptor;
1026 int newDescriptorId;
1027
1028 if (oldDescriptor.HasName)
1029 {
1030 //
1031 // Always lookup named instances, this deals with cases where an instance from one source file
1032 // is replaced by one from another source file.
1033 //
1034
1035 if (namedInstancedIdMap.TryGetValue(oldDescriptor.FullName, out newDescriptorId))
1036 return newDescriptorId;
1037 }
1038
1039 if (sharedMap.TryGetValue(oldDescriptor, out newDescriptor))
1040 return linkMaps[newDescriptor.File][newDescriptor.Index];
1041 }
1042
1043 return linkMaps[file][id >> 8];
1044 }
1045
1046 private void WriteBinaryParts(BinaryWriter writer, string filePath)
1047 {
1048 if (IsV32)
1049 {
1050 //
1051 // For .oni files the raw/sep parts are written to the .oni file.
1052 // Separate .raw/.sep files are not used.
1053 //
1054
1055 WriteParts(writer, rawParts);
1056 return;
1057 }
1058
1059 string rawFilePath = Path.ChangeExtension(filePath, ".raw");
1060
1061 Console.WriteLine("Writing {0}", rawFilePath);
1062
1063 using (var rawOutputStream = new FileStream(rawFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536))
1064 using (var rawWriter = new BinaryWriter(rawOutputStream))
1065 {
1066 WriteParts(rawWriter, rawParts);
1067 }
1068
1069 if (header.TemplateChecksum == InstanceFileHeader.OniMacTemplateChecksum)
1070 {
1071 //
1072 // Only Mac/PC Demo files have a .sep file.
1073 //
1074
1075 string sepFilePath = Path.ChangeExtension(filePath, ".sep");
1076
1077 Console.WriteLine("Writing {0}", sepFilePath);
1078
1079 using (var sepOutputStream = new FileStream(sepFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536))
1080 using (var sepWriter = new BinaryWriter(sepOutputStream))
1081 {
1082 WriteParts(sepWriter, sepParts);
1083 }
1084 }
1085 }
1086
1087 private void WriteParts(BinaryWriter writer, List<BinaryPartEntry> binaryParts)
1088 {
1089 if (binaryParts.Count == 0)
1090 {
1091 writer.Write(padding, 0, 32);
1092 return;
1093 }
1094
1095 binaryParts.Sort();
1096
1097 int fileLength = 0;
1098
1099 foreach (BinaryPartEntry entry in binaryParts)
1100 {
1101 if (entry.DestinationOffset + entry.Size > fileLength)
1102 fileLength = entry.DestinationOffset + entry.Size;
1103 }
1104
1105 if (IsV31)
1106 writer.BaseStream.SetLength(fileLength);
1107 else
1108 writer.BaseStream.SetLength(fileLength + header.RawTableOffset);
1109
1110 BinaryReader reader = null;
1111
1112 foreach (BinaryPartEntry entry in binaryParts)
1113 {
1114 if (entry.SourceFile == null)
1115 continue;
1116
1117 if (reader == null)
1118 {
1119 reader = new BinaryReader(entry.SourceFile);
1120 }
1121 else if (reader.Name != entry.SourceFile)
1122 {
1123 reader.Dispose();
1124 reader = new BinaryReader(entry.SourceFile);
1125 }
1126
1127 reader.Position = entry.SourceOffset;
1128
1129 //
1130 // Smart change of output's stream position. This assumes that
1131 // the binary parts are sorted by destination offset.
1132 //
1133
1134 int padSize = entry.DestinationOffset + header.RawTableOffset - writer.Position;
1135
1136 if (padSize <= 32)
1137 writer.Write(padding, 0, padSize);
1138 else
1139 writer.Position = entry.DestinationOffset + header.RawTableOffset;
1140
1141 if (entry.Field == null || entry.Field.RawType == null)
1142 {
1143 //
1144 // If we don't know the field or the fieldtype for this binary part
1145 // we just copy it over without cleaning up garbage.
1146 //
1147
1148 if (copyBuffer1.Length < entry.Size)
1149 copyBuffer1 = new byte[entry.Size * 2];
1150
1151 reader.Read(copyBuffer1, 0, entry.Size);
1152 writer.Write(copyBuffer1, 0, entry.Size);
1153 }
1154 else
1155 {
1156 int size = entry.Size;
1157
1158 while (size > 0)
1159 {
1160 int copiedSize = entry.Field.RawType.Copy(reader, writer, null);
1161
1162 if (copiedSize > size)
1163 throw new InvalidOperationException(string.Format("Bad metadata copying field {0}", entry.Field.Name));
1164
1165 size -= copiedSize;
1166 }
1167 }
1168 }
1169
1170 if (reader != null)
1171 reader.Dispose();
1172 }
1173
1174 private List<InstanceDescriptor> RemoveDuplicates(List<InstanceDescriptor> descriptors)
1175 {
1176 var checksums = new Dictionary<int, List<InstanceDescriptor>>();
1177 var newDescriptorList = new List<InstanceDescriptor>(descriptors.Count);
1178
1179 foreach (var descriptor in descriptors)
1180 {
1181 //
1182 // We only handle duplicates for these types of instances.
1183 // These are the most common cases and they are simple to handle
1184 // because they do not contain links to other instances.
1185 //
1186
1187 if (!(descriptor.Template.Tag == TemplateTag.IDXA
1188 || descriptor.Template.Tag == TemplateTag.PNTA
1189 || descriptor.Template.Tag == TemplateTag.VCRA
1190 || descriptor.Template.Tag == TemplateTag.TXCA
1191 || descriptor.Template.Tag == TemplateTag.TRTA
1192 || descriptor.Template.Tag == TemplateTag.TRIA
1193 || descriptor.Template.Tag == TemplateTag.ONCP
1194 || descriptor.Template.Tag == TemplateTag.ONIA))
1195 {
1196 newDescriptorList.Add(descriptor);
1197 continue;
1198 }
1199
1200 int checksum = GetInstanceChecksum(descriptor);
1201
1202 List<InstanceDescriptor> existingDescriptors;
1203
1204 if (!checksums.TryGetValue(checksum, out existingDescriptors))
1205 {
1206 existingDescriptors = new List<InstanceDescriptor>();
1207 checksums.Add(checksum, existingDescriptors);
1208 }
1209 else
1210 {
1211 InstanceDescriptor existing = existingDescriptors.Find(x => AreInstancesEqual(descriptor, x));
1212
1213 if (existing != null)
1214 {
1215 sharedMap.Add(descriptor, existing);
1216 continue;
1217 }
1218 }
1219
1220 existingDescriptors.Add(descriptor);
1221 newDescriptorList.Add(descriptor);
1222 }
1223
1224 return newDescriptorList;
1225 }
1226
1227 private int GetInstanceChecksum(InstanceDescriptor descriptor)
1228 {
1229 using (var checksumStream = new ChecksumStream())
1230 using (var writer = new BinaryWriter(checksumStream))
1231 {
1232 descriptor.Template.Type.Copy(streamCache.GetReader(descriptor), writer, null);
1233 return checksumStream.Checksum;
1234 }
1235 }
1236
1237 private bool AreInstancesEqual(InstanceDescriptor d1, InstanceDescriptor d2)
1238 {
1239 if (d1.File == d2.File && d1.Index == d2.Index)
1240 return true;
1241
1242 if (d1.Template.Tag != d2.Template.Tag
1243 || d1.DataSize != d2.DataSize)
1244 return false;
1245
1246 if (copyBuffer1.Length < d1.DataSize)
1247 copyBuffer1 = new byte[d1.DataSize * 2];
1248
1249 if (copyBuffer2.Length < d2.DataSize)
1250 copyBuffer2 = new byte[d2.DataSize * 2];
1251
1252 MetaType type = d1.Template.Type;
1253
1254 //return type.Compare(streamCache.GetStream(d1), streamCache.GetStream(d2));
1255
1256 using (var writer1 = new BinaryWriter(new MemoryStream(copyBuffer1)))
1257 using (var writer2 = new BinaryWriter(new MemoryStream(copyBuffer2)))
1258 {
1259 int s1 = type.Copy(streamCache.GetReader(d1), writer1, null);
1260 int s2 = type.Copy(streamCache.GetReader(d2), writer2, null);
1261
1262 if (s1 != s2)
1263 return false;
1264
1265 for (int i = 0; i < s1; i++)
1266 {
1267 if (copyBuffer1[i] != copyBuffer2[i])
1268 return false;
1269 }
1270 }
1271
1272 return true;
1273 }
1274
1275 private bool IsV31 => header.Version == InstanceFileHeader.Version31;
1276 private bool IsV32 => header.Version == InstanceFileHeader.Version32;
1277
1278 private static int MakeFileId(string filePath)
1279 {
1280 //
1281 // File id is generated from the filename. The filename is expected to be in
1282 // XXXXXN_YYYYY.dat format where the XXXXX (5 characters) part is ignored, N is treated
1283 // as a number (level number) and the YYYYY (this part can have any length) is hashed.
1284 // The file extension is ignored.
1285 //
1286
1287 string fileName = Path.GetFileNameWithoutExtension(filePath);
1288
1289 if (fileName.Length < 6)
1290 return 0;
1291
1292 fileName = fileName.Substring(5);
1293
1294 int levelNumber = 0;
1295 int buildTypeHash = 0;
1296
1297 int i = fileName.IndexOf('_');
1298
1299 if (i != -1)
1300 {
1301 int.TryParse(fileName.Substring(0, i), out levelNumber);
1302
1303 if (!string.Equals(fileName.Substring(i + 1), "Final", StringComparison.Ordinal))
1304 {
1305 for (int j = 1; i + j < fileName.Length; j++)
1306 buildTypeHash += (char.ToUpperInvariant(fileName[i + j]) - 0x40) * j;
1307 }
1308 }
1309
1310 return (((levelNumber << 24) | (buildTypeHash & 0xffffff)) << 1) | 1;
1311 }
1312
1313 public static int MakeInstanceId(int index) => (index << 8) | 1;
1314 }
1315}
Note: See TracBrowser for help on using the repository browser.