Beispiel #1
0
        public static int Execute(List <string> args)
        {
            if (args.Count < 2)
            {
                Console.WriteLine("Usage: V0000.SIF V0000.VP");
                return(-1);
            }

            char[]   comma     = new char[] { ',' };
            string[] siflines  = System.IO.File.ReadAllLines(args[0]);
            long     filecount = long.Parse(siflines[0].Split(comma)[0]);
            var      offsets   = new List <(long offset, long size, string name)>();

            for (long i = 1; i < siflines.LongLength; ++i)
            {
                string[] split = siflines[i].Split(comma, 3);
                if (split.Length >= 2)
                {
                    string name = null;
                    if (split.Length == 3)
                    {
                        name = TryParseFilename(split[2]);
                    }
                    offsets.Add((long.Parse(split[0]), long.Parse(split[1]), name));
                }
            }

            // make sure we have no duplicate filenames
            HashSet <string> filenames = new HashSet <string>();

            for (int i = 0; i < offsets.Count; ++i)
            {
                string filename = offsets[i].name ?? i.ToString("D4");
                string basename = filename;
                int    idx      = 2;
                while (filenames.Contains(filename))
                {
                    filename = basename + "_" + idx;
                    ++idx;
                }
                offsets[i] = (offsets[i].offset, offsets[i].size, filename);
                filenames.Add(filename);
            }

            string outbasename = System.IO.Path.GetFileNameWithoutExtension(args[1]);
            long   sectorsize  = 2048;

            using (var vp = new HyoutaUtils.Streams.DuplicatableFileStream(args[1])) {
                for (int i = 0; i < offsets.Count; ++i)
                {
                    using (var fs = new System.IO.FileStream(outbasename + "_" + offsets[i].name, System.IO.FileMode.Create)) {
                        vp.Position = offsets[i].offset * sectorsize;
                        HyoutaUtils.StreamUtils.CopyStream(vp, fs, offsets[i].size);
                    }
                }
            }

            return(0);
        }
Beispiel #2
0
        public static int Compress(List <string> args)
        {
            string inpath  = null;
            string outpath = null;

            try {
                for (int i = 0; i < args.Count; ++i)
                {
                    switch (args[i])
                    {
                    default:
                        if (inpath == null)
                        {
                            inpath = args[i];
                        }
                        else if (outpath == null)
                        {
                            outpath = args[i];
                        }
                        else
                        {
                            PrintCompressUsage(); return(-1);
                        }
                        break;
                    }
                }
            } catch (IndexOutOfRangeException) {
                PrintCompressUsage();
                return(-1);
            }

            if (inpath == null)
            {
                PrintCompressUsage();
                return(-1);
            }

            if (outpath == null)
            {
                outpath = inpath + ".cmp";
            }

            using (var infile = new HyoutaUtils.Streams.DuplicatableFileStream(inpath))
                using (var outfile = new FileStream(outpath, FileMode.Create)) {
                    utf_tab_sharp.CpkCompress.compress(infile, 0, infile.Length, outfile);
                }

            return(0);
        }
Beispiel #3
0
 private static HyoutaUtils.HyoutaArchive.HyoutaArchiveContainer TryLoadBackupArchive(string backupArchivePath, ProgressReporter progress)
 {
     try {
         if (File.Exists(backupArchivePath))
         {
             progress.Message(string.Format("Loading backup archive at {0}...", backupArchivePath));
             using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(backupArchivePath)) {
                 return(new HyoutaUtils.HyoutaArchive.HyoutaArchiveContainer(fs));
             }
         }
     } catch (Exception ex) {
         progress.Error(string.Format("Failed to load backup file archive: {0}", ex.Message));
     }
     return(null);
 }
        public static int Execute(List <string> args)
        {
            if (args.Count < 1)
            {
                Console.WriteLine("Usage: infile.txt [outfile.bps]");
                return(-1);
            }

            string inpath  = args[0];
            string outpath = args.Count >= 2 ? args[1] : (inpath + ".bps");

            using (var instream = new HyoutaUtils.Streams.DuplicatableFileStream(inpath))
                using (var outstream = new FileStream(outpath, FileMode.Create)) {
                    TextToBpsConverter.GeneratePatchFromText(instream, outstream);
                }

            return(0);
        }
Beispiel #5
0
        // this is not a generic wav parser, this just works for diffing two already similar files
        public wav_samples(string filename)
        {
            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(filename)) {
                var  e          = EndianUtils.Endianness.LittleEndian;
                uint riff       = fs.ReadUInt32(e);
                uint rifflength = fs.ReadUInt32(e);
                uint wave       = fs.ReadUInt32(e);

                uint   fmt              = fs.ReadUInt32(e);
                uint   fmt_length       = fs.ReadUInt32(e);
                ushort format           = fs.ReadUInt16(e);
                ushort channels         = fs.ReadUInt16(e);
                uint   samplerate       = fs.ReadUInt32(e);
                uint   bytes_per_second = fs.ReadUInt32(e);
                ushort block_align      = fs.ReadUInt16(e);
                ushort bits_per_sample  = fs.ReadUInt16(e);
                uint   data             = fs.ReadUInt32(e);
                uint   data_length      = fs.ReadUInt32(e);
                if (channels == 1)
                {
                    long    sample_count = data_length / 2;
                    short[] samples      = new short[sample_count];
                    for (long i = 0; i < sample_count; ++i)
                    {
                        samples[i] = fs.ReadInt16(e);
                    }
                    Samples_L = samples;
                }
                else if (channels == 2)
                {
                    long    sample_count  = data_length / 4;
                    short[] samples_left  = new short[sample_count];
                    short[] samples_right = new short[sample_count];
                    for (long i = 0; i < sample_count; ++i)
                    {
                        samples_left[i]  = fs.ReadInt16(e);
                        samples_right[i] = fs.ReadInt16(e);
                    }
                    Samples_L = samples_left;
                    Samples_R = samples_right;
                }
            }
        }
Beispiel #6
0
        public static int ExecuteCreate(List <string> args)
        {
            if (args.Count < 3)
            {
                Console.WriteLine("Usage: source.bin target.bin patch.bps");
                return(-1);
            }

            string sourcepath = args[0];
            string targetpath = args[1];
            string patchpath  = args[2];

            using (var sourcestream = new HyoutaUtils.Streams.DuplicatableFileStream(sourcepath))
                using (var targetstream = new HyoutaUtils.Streams.DuplicatableFileStream(targetpath))
                    using (var outstream = new FileStream(patchpath, FileMode.Create)) {
                        CreateSimplest.CreatePatch(sourcestream, targetstream, outstream);
                    }

            return(0);
        }
Beispiel #7
0
        public static int Main(string[] args)
        {
            if (args.Length >= 2 && (args[0] == "--parse-script" || args[0] == "--parse-book"))
            {
                using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(args[1])) {
                    var funcs = ScriptParser.Parse(fs.CopyToByteArrayStreamAndDispose(), args[0] == "--parse-book");

                    using (var outfs = new FileStream(args.Length > 2 ? args[2] : args[1] + ".txt", FileMode.Create)) {
                        foreach (var func in funcs)
                        {
                            outfs.WriteUTF8(func.Name);
                            outfs.WriteUTF8("\n");
                            outfs.WriteUTF8("\n");
                            foreach (var op in func.Ops)
                            {
                                outfs.WriteUTF8(op);
                                outfs.WriteUTF8("\n");
                            }
                            outfs.WriteUTF8("\n");
                            outfs.WriteUTF8("\n");
                        }
                    }
                }
                return(0);
            }

            if (args.Length == 1 && args[0] == "__gen_voice_checks")
            {
                t_voice_tbl.CheckVoiceTable(
                    Path.Combine(SenCommonPaths.Sen1SteamDir, "data/text/dat_us/t_voice.tbl"),
                    Path.Combine(SenCommonPaths.Sen1SteamDir, "data/voice/wav"),
                    Path.Combine(SenCommonPaths.Sen1SteamDir, "voice_check_english.txt")
                    );
                t_voice_tbl.CheckVoiceTable(
                    Path.Combine(SenCommonPaths.Sen1SteamDir, "data/text/dat/t_voice.tbl"),
                    Path.Combine(SenCommonPaths.Sen1SteamDir, "data/voice/wav_jp"),
                    Path.Combine(SenCommonPaths.Sen1SteamDir, "voice_check_japanese.txt")
                    );
                t_voice_tbl.CheckVoiceTable(
                    Path.Combine(SenCommonPaths.Sen2SteamDir, "data/text/dat_us/t_voice.tbl"),
                    Path.Combine(SenCommonPaths.Sen2SteamDir, "data/voice/wav"),
                    Path.Combine(SenCommonPaths.Sen2SteamDir, "voice_check_english.txt")
                    );
                t_voice_tbl.CheckVoiceTable(
                    Path.Combine(SenCommonPaths.Sen2SteamDir, "data/text/dat/t_voice.tbl"),
                    Path.Combine(SenCommonPaths.Sen2SteamDir, "data/voice_jp/wav"),
                    Path.Combine(SenCommonPaths.Sen2SteamDir, "voice_check_japanese.txt")
                    );
                return(0);
            }

            if (args.Length == 0)
            {
                Console.WriteLine("No path to directory given.");
                return(-1);
            }

            string path = args[0];

            if (!Directory.Exists(path))
            {
                Console.WriteLine($"No directory found at {path}.");
                return(-1);
            }

            int sengame;

            if (File.Exists(System.IO.Path.Combine(path, "Sen1Launcher.exe")))
            {
                sengame = 1;
            }
            else if (File.Exists(System.IO.Path.Combine(path, "Sen2Launcher.exe")))
            {
                sengame = 2;
            }
            else
            {
                Console.WriteLine($"Failed to detect whether {path} is CS1 or 2.");
                return(-1);
            }

            FilenameFix.FixupIncorrectEncodingInFilenames(path, sengame, true, new CliProgressReporter());
            FileStorage storage = FileModExec.InitializeAndPersistFileStorage(path, sengame == 1 ? Sen1KnownFiles.Files : Sen2KnownFiles.Files, new CliProgressReporter())?.Storage;

            if (storage == null)
            {
                Console.WriteLine($"Failed to initialize file storage from {path}.");
                return(-1);
            }

            PatchResult result;

            if (sengame == 1)
            {
                var mods = new List <FileMod>();
                mods.AddRange(Sen1Mods.GetExecutableMods(
                                  removeTurboSkip: true,
                                  allowR2NotebookShortcut: true,
                                  turboKey: 0xA,
                                  fixTextureIds: true,
                                  correctLanguageVoiceTables: true,
                                  disableMouseCapture: true,
                                  disablePauseOnFocusLoss: true
                                  ));
                mods.AddRange(Sen1Mods.GetAssetMods());
                result = FileModExec.ExecuteMods(path, storage, mods, new CliProgressReporter());
            }
            else
            {
                var mods = new List <FileMod>();
                mods.AddRange(Sen2Mods.GetExecutableMods(
                                  removeTurboSkip: true,
                                  patchAudioThread: true,
                                  audioThreadDivisor: 1000,
                                  patchBgmQueueing: true,
                                  correctLanguageVoiceTables: true,
                                  disableMouseCapture: true,
                                  disablePauseOnFocusLoss: true
                                  ));
                mods.AddRange(Sen2Mods.GetAssetMods());
                result = FileModExec.ExecuteMods(path, storage, mods, new CliProgressReporter());
            }

            if (!result.AllSuccessful)
            {
                Console.WriteLine($"Failed to patch CS{sengame} at {path}.");
                return(-1);
            }

            Console.WriteLine($"Successfully patched CS{sengame} at {path}.");
            return(0);
        }
Beispiel #8
0
            public void Cs1GameInit()
            {
                int  CurrentProgress       = 0;
                int  TotalProgress         = 4;
                bool shouldAutoCloseWindow = true;

                try {
                    Progress.Message("Checking Sen1Launcher.exe...", CurrentProgress++, TotalProgress);
                    using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(Sen1LauncherPath)) {
                        SHA1 hash = ChecksumUtils.CalculateSHA1ForEntireStream(fs);
                        if (hash != new SHA1(0x8dde2b39f128179aul, 0x0beb3301cfd56a98ul, 0xc0f98a55u))
                        {
                            Progress.Error("Selected file does not appear to be Sen1Launcher.exe of version 1.6.");
                            Progress.Finish(false);
                            return;
                        }
                    }
                } catch (Exception ex) {
                    Progress.Error("Error while validating Sen1Launcher.exe: " + ex.Message);
                    Progress.Finish(false);
                    return;
                }

                try {
                    Path = System.IO.Path.GetDirectoryName(Sen1LauncherPath);
                    Progress.Message("Checking if we have encoding errors in filenames...", CurrentProgress++, TotalProgress);
                    if (FilenameFix.FixupIncorrectEncodingInFilenames(Path, 1, false, Progress))
                    {
                        if (!FilenameFix.FixupIncorrectEncodingInFilenames(Path, 1, true, Progress))
                        {
                            Progress.Error("Failed to fix encoding errors in filenames, attempting to proceed anyway...");
                            shouldAutoCloseWindow = false;
                        }
                    }
                    Progress.Message("Initializing patch data...", CurrentProgress++, TotalProgress);
                    var files = Sen1KnownFiles.Files;
                    Progress.Message("Initializing game data...", CurrentProgress++, TotalProgress);
                    var storageInit = FileModExec.InitializeAndPersistFileStorage(Path, files, Progress);
                    Storage = storageInit?.Storage;
                    if (storageInit == null || storageInit.Errors.Count != 0)
                    {
                        shouldAutoCloseWindow = false;
                    }
                } catch (Exception ex) {
                    Progress.Error("Error while initializing CS1 patch/game data: " + ex.Message);
                    Progress.Finish(false);
                    return;
                }

                ShouldProceedToPatchOptionWindow = Path != null && Storage != null;
                if (shouldAutoCloseWindow)
                {
                    Progress.Message("Initialized CS1 data, proceeding to patch options...", CurrentProgress, TotalProgress);
                }
                else
                {
                    Progress.Message("", CurrentProgress, TotalProgress);
                    if (ShouldProceedToPatchOptionWindow)
                    {
                        Progress.Error(
                            "Encountered problems while initializing CS1 data. "
                            + "Closing this window will proceed to the patch options anyway, but be aware that some patches may not work correctly. "
                            + "It is recommended to verify the game files using Steam or GOG Galaxy's build-in feature to do so, or to reinstall the game. "
                            + "Please also ensure you're trying to patch a compatible version of the game. (XSEED release version 1.6; other game versions are not compatible)"
                            );
                    }
                    else
                    {
                        Progress.Error(
                            "Unrecoverable issues while initializing CS1 data. "
                            + "Please ensure SenPatcher has read and write access to the selected game directory, then try again."
                            );
                    }
                }
                Progress.Finish(shouldAutoCloseWindow);
            }
        public static void GenerateEnFiles(bool prefer_ps3_over_ps4)
        {
            // we pretty much do the same as JP here but we have to do an ID remapping, because the PS4 internal IDs for the PC audio files are different
            VoiceTable canonicalEnVoiceTable;

            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(@"c:\_tmp_a\_cs1-voicetiming\__pc-senpatcher\t_voice_US.tbl")) {
                canonicalEnVoiceTable = new VoiceTable(fs, EndianUtils.Endianness.LittleEndian);
            }
            VoiceTable ps4EnVoiceTable;

            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(@"c:\_tmp_a\_cs1-voicetiming\_ps4-us\t_voice.tbl")) {
                ps4EnVoiceTable = new VoiceTable(fs, EndianUtils.Endianness.LittleEndian);
            }

            Dictionary <string, List <VoiceTableEntry> > pcFilenameToInternalIdMap = new Dictionary <string, List <VoiceTableEntry> >();

            foreach (var a in canonicalEnVoiceTable.Entries)
            {
                if (!pcFilenameToInternalIdMap.ContainsKey(a.Name))
                {
                    pcFilenameToInternalIdMap.Add(a.Name, new List <VoiceTableEntry>());
                }
                pcFilenameToInternalIdMap[a.Name].Add(a);
            }
            Dictionary <string, List <VoiceTableEntry> > ps4FilenameToInternalIdMap = new Dictionary <string, List <VoiceTableEntry> >();

            foreach (var a in ps4EnVoiceTable.Entries)
            {
                string n = a.Name == "pc8v10286" ? "pc8v10299" : a.Name == "pc8v10286_6" ? "pc8v10286" : a.Name;                 // need to match my remapping of the overwritten sara line
                if (!ps4FilenameToInternalIdMap.ContainsKey(n))
                {
                    ps4FilenameToInternalIdMap.Add(n, new List <VoiceTableEntry>());
                }
                ps4FilenameToInternalIdMap[n].Add(a);
            }

            if (pcFilenameToInternalIdMap.Count != ps4FilenameToInternalIdMap.Count)
            {
                // shouldn't happen with correct input
                Console.WriteLine($"global count mismatch PC {pcFilenameToInternalIdMap.Count} <=> PS4 {ps4FilenameToInternalIdMap.Count}");
            }

            foreach (var kvp in pcFilenameToInternalIdMap)
            {
                var pc  = kvp.Value;
                var ps4 = ps4FilenameToInternalIdMap[kvp.Key];
                if (pc.Count != ps4.Count)
                {
                    if (pc.Count == 2 && ps4.Count == 1)
                    {
                        // just dupe the PS4 entry to get it to match 1:1
                        ps4.Add(new VoiceTableEntry(ps4[0]));
                    }
                    else
                    {
                        // shouldn't happen with correct input
                        Console.WriteLine($"count mismatch PC {pc.Count} <=> PS4 {ps4.Count}");
                    }
                }
            }

            // okay now we have a 1:1 mapping between PC and PS4 IDs; we can apply this on the PS4 voice timing file to get a PC-compatible one

            VoiceTiming enPs3PcVoiceTiming;             // is the same file in both versions, just endian-flipped

            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(@"c:\_tmp_a\_cs1-voicetiming\_pc-us\t_vctiming.tbl")) {
                enPs3PcVoiceTiming = new VoiceTiming(fs, EndianUtils.Endianness.LittleEndian);
            }

            VoiceTiming enPs4USVoiceTiming;

            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(@"c:\_tmp_a\_cs1-voicetiming\_ps4-us\t_vctiming_us.tbl")) {
                enPs4USVoiceTiming = new VoiceTiming(fs, EndianUtils.Endianness.LittleEndian);
            }

            SortedDictionary <ushort, VoiceTimingEntry> voiceTimingDict = new SortedDictionary <ushort, VoiceTimingEntry>();

            foreach (VoiceTimingEntry vte in enPs3PcVoiceTiming.Entries)
            {
                if (voiceTimingDict.ContainsKey(vte.Index))
                {
                    Console.WriteLine($"index {vte.Index} mapped more than once?");
                    var existingMapping = voiceTimingDict[vte.Index];
                    if (existingMapping.TimingData != vte.TimingData)
                    {
                        Console.WriteLine($"timing data mismatch: {existingMapping.TimingData.ToString("x16")} <=> {vte.TimingData.ToString("x16")}");
                    }
                    voiceTimingDict.Remove(vte.Index);
                }
                voiceTimingDict.Add(vte.Index, vte);
            }

            HashSet <ushort> mappedIndices = new HashSet <ushort>();

            foreach (VoiceTableEntry vte in canonicalEnVoiceTable.Entries)
            {
                if (!mappedIndices.Contains(vte.Index))
                {
                    mappedIndices.Add(vte.Index);
                }
            }

            foreach (VoiceTimingEntry vte in enPs4USVoiceTiming.Entries)
            {
                List <VoiceTableEntry> specialPcEntries  = null;
                List <VoiceTableEntry> specialPs4Entries = null;
                ushort remappedIndex = RemapPs4ToPc(vte.Index, pcFilenameToInternalIdMap, ps4FilenameToInternalIdMap, ref specialPcEntries, ref specialPs4Entries);

                ushort[] indices;
                if (specialPcEntries != null)
                {
                    indices = new ushort[2] {
                        specialPcEntries[0].Index, specialPcEntries[1].Index
                    };
                }
                else
                {
                    indices = new ushort[1] {
                        remappedIndex
                    };
                }

                foreach (ushort index in indices)
                {
                    if (mappedIndices.Contains(index))
                    {
                        if (prefer_ps3_over_ps4)
                        {
                            // only insert if there's not already an entry
                            if (!voiceTimingDict.ContainsKey(index))
                            {
                                VoiceTimingEntry vte2 = new VoiceTimingEntry(vte);
                                vte2.Index = index;
                                voiceTimingDict.Add(index, vte2);
                            }
                        }
                        else
                        {
                            // overwrite existing entries
                            if (voiceTimingDict.ContainsKey(index))
                            {
                                voiceTimingDict.Remove(index);
                            }
                            VoiceTimingEntry vte2 = new VoiceTimingEntry(vte);
                            vte2.Index = index;
                            voiceTimingDict.Add(index, vte2);
                        }
                    }
                }
            }

            enPs3PcVoiceTiming.Entries.Clear();
            foreach (var kvp in voiceTimingDict)
            {
                enPs3PcVoiceTiming.Entries.Add(kvp.Value);
            }
            using (var fs = new FileStream(@"c:\_tmp_a\_cs1-voicetiming\__pc-senpatcher\__gen_vctiming_us_ps" + (prefer_ps3_over_ps4 ? "3" : "4") + "_variant.tbl", FileMode.Create)) {
                enPs3PcVoiceTiming.WriteToStream(fs, EndianUtils.Endianness.LittleEndian);
            }
        }
        public static void GenerateJpFiles(bool prefer_ps3_over_ps4)
        {
            VoiceTable canonicalJpVoiceTable;

            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(@"c:\_tmp_a\_cs1-voicetiming\__pc-senpatcher\t_voice_JP.tbl")) {
                canonicalJpVoiceTable = new VoiceTable(fs, EndianUtils.Endianness.LittleEndian);
            }

            VoiceTiming jpPs3VoiceTiming;

            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(@"c:\_tmp_a\_cs1-voicetiming\_ps3-jp\t_vctiming.tbl")) {
                jpPs3VoiceTiming = new VoiceTiming(fs, EndianUtils.Endianness.BigEndian);
            }

            VoiceTiming jpPs4USVoiceTiming;

            using (var fs = new HyoutaUtils.Streams.DuplicatableFileStream(@"c:\_tmp_a\_cs1-voicetiming\_ps4-us\t_vctiming.tbl")) {
                jpPs4USVoiceTiming = new VoiceTiming(fs, EndianUtils.Endianness.LittleEndian);
            }

            SortedDictionary <ushort, VoiceTimingEntry> voiceTimingDict = new SortedDictionary <ushort, VoiceTimingEntry>();

            foreach (VoiceTimingEntry vte in jpPs3VoiceTiming.Entries)
            {
                if (voiceTimingDict.ContainsKey(vte.Index))
                {
                    Console.WriteLine($"index {vte.Index} mapped more than once?");
                    voiceTimingDict.Remove(vte.Index);
                }
                voiceTimingDict.Add(vte.Index, vte);
            }

            HashSet <ushort> mappedIndices = new HashSet <ushort>();

            foreach (VoiceTableEntry vte in canonicalJpVoiceTable.Entries)
            {
                if (!mappedIndices.Contains(vte.Index))
                {
                    mappedIndices.Add(vte.Index);
                }
            }

            foreach (VoiceTimingEntry vte in jpPs4USVoiceTiming.Entries)
            {
                if (mappedIndices.Contains(vte.Index))
                {
                    if (prefer_ps3_over_ps4)
                    {
                        // only insert if there's not already an entry
                        if (!voiceTimingDict.ContainsKey(vte.Index))
                        {
                            voiceTimingDict.Add(vte.Index, vte);
                        }
                    }
                    else
                    {
                        // overwrite existing entries
                        if (voiceTimingDict.ContainsKey(vte.Index))
                        {
                            voiceTimingDict.Remove(vte.Index);
                        }
                        voiceTimingDict.Add(vte.Index, vte);
                    }
                }
            }


            jpPs3VoiceTiming.Entries.Clear();
            foreach (var kvp in voiceTimingDict)
            {
                jpPs3VoiceTiming.Entries.Add(kvp.Value);
            }
            using (var fs = new FileStream(@"c:\_tmp_a\_cs1-voicetiming\__pc-senpatcher\__gen_vctiming_jp_ps" + (prefer_ps3_over_ps4 ? "3" : "4") + "_variant.tbl", FileMode.Create)) {
                jpPs3VoiceTiming.WriteToStream(fs, EndianUtils.Endianness.LittleEndian);
            }
        }