├── .gitignore ├── Galgame ├── 1.py ├── 2.py ├── 4.py ├── 5.py ├── 6.py ├── ArkEngine │ ├── 1.py │ └── 2.py ├── ArtemisEngine │ ├── File.py │ ├── TextCleaner_ArtemisEngine_lua.py │ └── TextCleaner_ArtemisEngine_txt.py ├── Black Cyc engine │ └── 1.py ├── BlackRainbow │ └── 1.py ├── CIRCUS │ ├── 1.py │ ├── 2.py │ ├── 3.py │ └── vgmstream-win64 │ │ ├── COPYING │ │ ├── README.md │ │ ├── USAGE.md │ │ ├── avcodec-vgmstream-59.dll │ │ ├── avformat-vgmstream-59.dll │ │ ├── avutil-vgmstream-57.dll │ │ ├── libatrac9.dll │ │ ├── libcelt-0061.dll │ │ ├── libcelt-0110.dll │ │ ├── libg719_decode.dll │ │ ├── libmpg123-0.dll │ │ ├── libspeex-1.dll │ │ ├── libvorbis.dll │ │ ├── swresample-vgmstream-4.dll │ │ └── vgmstream-cli.exe ├── GSIWin │ └── 1.py ├── Mirai │ └── 1.py ├── NeXAS │ ├── 1.py │ ├── tool │ │ ├── constants.py │ │ ├── disassembler.py │ │ ├── message.py │ │ ├── reader.py │ │ └── script_data.py │ ├── version_0.yaml │ ├── version_1.yaml │ └── version_2.yaml ├── NeXAS_Unity │ └── spm.py ├── NekoSDK │ └── 1.py ├── Noesis │ └── 1.py ├── README.md ├── SiglusEngine │ ├── TextCleaner_SiglusEngine.exe │ ├── TextCleaner_SiglusEngine.ini │ ├── TextCleaner_SiglusEngine.py │ └── TextCleaner_SiglusEngine_File.py ├── TextCleaner_ADVPlayerHD.py ├── TextCleaner_BGI.py ├── TextCleaner_BGI_C.py ├── TextCleaner_CMVS.py ├── TextCleaner_CatSystem2.py ├── TextCleaner_CatSystemUnity.py ├── TextCleaner_DATEALIVE.py ├── TextCleaner_DebonosuWorks.py ├── TextCleaner_FAVORITE.py ├── TextCleaner_File.py ├── TextCleaner_Genshin_StarRail_CN.py ├── TextCleaner_MCSM.py ├── TextCleaner_QLIE.py ├── TextCleaner_ShiinaRio.py ├── TextCleaner_SiglusEngine.ini ├── TextCleaner_Will Co.py ├── TextCleaner_alice.py ├── TextCleaner_minori.py ├── Text_Cleaner_Stack.py ├── YuRis │ ├── TextCleaner_YuRis.py │ ├── TextCleaner_YuRis_YSCM.py │ ├── TextCleaner_YuRis_YSTB.py │ └── TextCleaner_YuRis_YSTL.py ├── a3.py ├── krkr │ ├── TextCleaner_Kirikiri_ks.py │ ├── TextCleaner_Kirikiri_scn.py │ ├── TextCleaner_Kirikiri_scn_Psb.exe │ ├── TextCleaner_Kirikiri_scn_Psb.exe.config │ ├── TextCleaner_Kirikiri_scn_Psb.py │ ├── TextCleaner_Kirikiri_scnname.py │ ├── krkr_cxdec_namestore.py │ └── lib │ │ ├── BCnEncoder.dll │ │ ├── Be.IO.dll │ │ ├── FastBitmapLib.dll │ │ ├── FreeMote.FastLz.dll │ │ ├── FreeMote.NET.dll │ │ ├── FreeMote.Plugins.dll │ │ ├── FreeMote.Plugins.x64.dll │ │ ├── FreeMote.PsBuild.dll │ │ ├── FreeMote.Psb.dll │ │ ├── FreeMote.dll │ │ ├── K4os.Compression.LZ4.Streams.dll │ │ ├── K4os.Compression.LZ4.dll │ │ ├── K4os.Hash.xxHash.dll │ │ ├── McMaster.Extensions.CommandLineUtils.dll │ │ ├── Microsoft.Bcl.HashCode.dll │ │ ├── Microsoft.IO.RecyclableMemoryStream.dll │ │ ├── Microsoft.Toolkit.HighPerformance.dll │ │ ├── Newtonsoft.Json.dll │ │ ├── PhotoShop.dll │ │ ├── System.Buffers.dll │ │ ├── System.IO.Pipelines.dll │ │ ├── System.Memory.dll │ │ ├── System.Numerics.Vectors.dll │ │ ├── System.Runtime.CompilerServices.Unsafe.dll │ │ ├── System.Threading.Tasks.Extensions.dll │ │ ├── System.ValueTuple.dll │ │ ├── TlgLib.dll │ │ ├── Troschuetz.Random.dll │ │ ├── VGAudio.dll │ │ ├── XMemCompress.dll │ │ ├── XmpCore.dll │ │ ├── ZstdNet.dll │ │ └── emotedriver.dll ├── luac.exe └── sprite │ ├── TextCleaner_sprite.py │ └── decrypt.py ├── Other ├── Calculater_Total_Duration.py ├── Calculater_Total_Duration_F.py ├── Combiner_Batch.py ├── Combiner_To_6s.py ├── Cutter.py ├── Deleter_Blank.py ├── Deleter_Over_30s.py ├── Resampler.py └── SAMI.py ├── README.md ├── Unity ├── BanG Dream! │ ├── downloader.py │ ├── script.py │ ├── tools │ │ ├── application_info.proto │ │ ├── application_info_pb2.py │ │ ├── assetbundle_info.proto │ │ └── assetbundle_info_pb2.py │ └── unpacker.py ├── Fate Grand Order │ ├── CpkMaker.dll │ ├── YACpkTool.exe │ ├── dec_assetbundle.py │ ├── dec_common.py │ ├── dec_pck.py │ ├── dec_script.py │ ├── downloader.py │ ├── fetch_index.py │ └── unpacker.py ├── Gakuen IDOLM@STER │ ├── downloader.py │ ├── script.py │ └── tools │ │ ├── octo.proto │ │ └── octo_pb2.py ├── Heaven Burns Red │ └── dec_script.py ├── Princess Connect! Re Dive │ ├── downloader.py │ ├── script.py │ └── unpacker.py ├── THE IDOLM@STER CINDERELLA GIRLS │ ├── downloader.py │ ├── script.py │ └── unpacker.py ├── Unpacker.py ├── Utage │ └── DicingTextures.py └── cppdael-0.0.2.3-cp312-cp312-win_amd64.whl └── Unreal_Engine ├── InfinityNikki └── TextCleaner_InfinityNikki_01.py └── WutheringWaves ├── TextCleaner_WutheringWaves_01.py ├── TextCleaner_WutheringWaves_02.py └── TextCleaner_WutheringWaves_03.py /Galgame/1.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import shutil 4 | 5 | ba = r'D:\Fuck_galgame\voice' 6 | 7 | # 读取 index.json 文件 8 | with open(r'D:\Fuck_galgame\index.json', 'r', encoding='utf-8') as f: 9 | index_data = json.load(f) 10 | 11 | # 缓存音频文件的路径 12 | audio_files = {} 13 | for root, dirs, files in os.walk(ba): 14 | for file in files: 15 | if file.endswith('.opus'): # 只缓存 .ogg 文件 16 | audio_files[file.lower()] = os.path.join(root, file) 17 | 18 | # 更新的 index 数据 19 | updated_index = [] 20 | 21 | # 遍历 index.json 中的每个条目 22 | for entry in index_data: 23 | speaker = entry['Speaker'].lower() # 获取 Speaker 名称并转换为小写 24 | voice = entry['Voice'].lower() # 获取 Voice 名称并转换为小写 25 | entry['Speaker'] = speaker # 更新 Speaker 名称 26 | entry['Voice'] = voice # 更新 Voice 名称 27 | # 拼接音频文件名 28 | audio_file_name = f"{voice}.opus" 29 | 30 | # 查找音频文件路径 31 | audio_file_path = audio_files.get(audio_file_name) 32 | 33 | if audio_file_path: 34 | # 创建以 Speaker 为名称的文件夹(如果不存在) 35 | speaker_folder = os.path.join(ba.replace('voice', 'f_'), speaker) 36 | if not os.path.exists(speaker_folder): 37 | os.makedirs(speaker_folder) 38 | # 移动音频文件到新的文件夹并转换为小写 39 | shutil.copy(audio_file_path, os.path.join(speaker_folder, audio_file_name)) 40 | # 添加条目到更新的 index 数据 41 | updated_index.append(entry) 42 | else: 43 | print(f"音频文件 {audio_file_name} 未找到") 44 | 45 | # 保存更新后的 index.json 46 | output_index_path = os.path.join(ba.replace('voice', 'f_'), 'index.json') 47 | with open(output_index_path, 'w', encoding='utf-8') as f: 48 | json.dump(updated_index, f, ensure_ascii=False, indent=4) -------------------------------------------------------------------------------- /Galgame/2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import shutil 4 | 5 | ba = r'D:\Fuck_galgame\vo' 6 | 7 | # 读取 index.json 文件 8 | with open(r'D:\Fuck_galgame\index.json', 'r', encoding='utf-8') as f: 9 | index_data = json.load(f) 10 | 11 | # 缓存音频文件的路径 12 | audio_files = {} 13 | for root, dirs, files in os.walk(ba): 14 | for file in files: 15 | audio_files[file.lower()] = os.path.join(root, file) 16 | 17 | # 更新的 index 数据 18 | updated_index = [] 19 | 20 | # 遍历 index.json 中的每个条目 21 | for entry in index_data: 22 | speaker = entry['Speaker'].lower() # 获取 Speaker 名称并转换为小写 23 | voice = entry['Voice'].lower() # 获取 Voice 名称并转换为小写 24 | entry['Speaker'] = speaker # 更新 Speaker 名称 25 | entry['Voice'] = voice # 更新 Voice 名称 26 | # 拼接音频文件名 27 | audio_file_name = voice # 源文件没有后缀 28 | 29 | # 查找音频文件路径 30 | audio_file_path = audio_files.get(audio_file_name) 31 | 32 | if audio_file_path: 33 | # 创建以 Speaker 为名称的文件夹(如果不存在) 34 | speaker_folder = os.path.join(ba.replace('vo', 'f'), speaker) 35 | if not os.path.exists(speaker_folder): 36 | os.makedirs(speaker_folder) 37 | # 移动音频文件到新的文件夹并添加 .ogg 后缀 38 | new_audio_file_path = os.path.join(speaker_folder, f"{audio_file_name}.ogg") 39 | shutil.move(audio_file_path, new_audio_file_path) 40 | # 添加条目到更新的 index 数据 41 | updated_index.append(entry) 42 | else: 43 | print(f"音频文件 {audio_file_name} 未找到") 44 | 45 | # 保存更新后的 index.json 46 | output_index_path = os.path.join(ba.replace('vo', 'f'), 'index.json') 47 | with open(output_index_path, 'w', encoding='utf-8') as f: 48 | json.dump(updated_index, f, ensure_ascii=False, indent=4) 49 | 50 | print("文件分类整理完成,新的 index.json 已保存") -------------------------------------------------------------------------------- /Galgame/4.py: -------------------------------------------------------------------------------- 1 | from a3 import Blowfish 2 | 3 | # V_CODE 4 | resource = b'\x6F\x06\xFF\xF6\xD6\x00\xD2\x4D\xC1\x70\xE1\xD3\x6F\xF5\xB2\x7D' 5 | 6 | # KEY_CODE 7 | key_resource = b'\x8B\x9F\x82\x83\x99\x9A\x84\x83\x8A' 8 | key = bytearray(key_resource) 9 | for i in range(len(key)): 10 | key[i] ^= 205 11 | key = bytes(key) 12 | 13 | # 初始化 Blowfish 密码器(ECB 模式) 14 | cipher = Blowfish(key) 15 | 16 | # 确保数据长度为 8 的倍数,并进行解密 17 | decrypt_length = (len(resource) // 8) * 8 18 | decrypted = cipher.decrypt(resource[:decrypt_length]) 19 | 20 | # 查找第一个 0 字节的位置 21 | try: 22 | num2 = decrypted.index(0) 23 | except ValueError: 24 | num2 = len(decrypted) 25 | 26 | # 使用 cp932 编码解码字符串 27 | decoded_text = decrypted[:num2].decode('cp932', errors='ignore') 28 | 29 | print(decoded_text) -------------------------------------------------------------------------------- /Galgame/6.py: -------------------------------------------------------------------------------- 1 | from lupa import LuaRuntime 2 | 3 | lua = LuaRuntime(unpack_returned_tuples=True) 4 | 5 | with open('0108.scb') as f: 6 | a = lua.execute(f.read()) 7 | 8 | print(a) -------------------------------------------------------------------------------- /Galgame/ArkEngine/1.py: -------------------------------------------------------------------------------- 1 | import os 2 | import msgpack 3 | import struct 4 | from tqdm import tqdm 5 | from Crypto.Cipher import AES 6 | from Crypto.Util.Padding import unpad 7 | 8 | PackEncryptKey = 'ARC-PACKPASSWORD'.encode('utf-8') 9 | EncryptKey = 'c6eahbq9sjuawhvdr9kvhpsm5qv393ga'.encode('utf-8') 10 | 11 | def read_idx(idx_path): 12 | pack_infos = [] 13 | with open(idx_path, 'rb') as f: 14 | while True: 15 | len_bytes = f.read(4) 16 | if not len_bytes: 17 | break 18 | data_len = struct.unpack(' 7 | Portions Copyright (c) 1998, Justin Frankel/Nullsoft Inc. 8 | Portions Copyright (C) 2006 Nullsoft, Inc. 9 | Portions Copyright (c) 2005-2007 Paul Hsieh 10 | Portions Public Domain originating with Sun Microsystems 11 | 12 | Permission to use, copy, modify, and distribute this software for any 13 | purpose with or without fee is hereby granted, provided that the above 14 | copyright notice and this permission notice appear in all copies. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 17 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 18 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 19 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 20 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 21 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 22 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 23 | -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/README.md: -------------------------------------------------------------------------------- 1 | # vgmstream 2 | This is vgmstream, a library for playing streamed (prerecorded) video game audio. 3 | 4 | Some of vgmstream's features: 5 | - Decodes [hundreds of video game music formats and codecs](doc/FORMATS.md), from typical 6 | game engine files to obscure single-game codecs, aiming for high accuracy and compatibility. 7 | - Support for looped BGM, using file's internal metadata for smooth transitions, with accurate 8 | sample counts. 9 | - [Subsongs](doc/USAGE.md#subsongs), playing a format's multiple internal songs separately. 10 | - Many types of companion files (data split into multiple files) and custom containers. 11 | - Encryption keys, internal stream names, and other unusual cases found in game audio. 12 | - [TXTH](doc/TXTH.md) function, to add external support for extra formats, including raw audio in 13 | many forms. 14 | - [TXTP](doc/TXTP.md) function, for real-time and per-file config, like forced looping, removing 15 | channels, playing certain subsong, or fusing multiple files into a single one. 16 | - Simple [external tagging](doc/USAGE.md#tagging) via .m3u files. 17 | - [Plugins](#getting-vgmstream) are available for various media player software and operating systems. 18 | 19 | The main development repository: https://github.com/vgmstream/vgmstream/ 20 | 21 | Automated builds with the latest changes: https://vgmstream.org 22 | (https://github.com/vgmstream/vgmstream-releases/releases/tag/nightly) 23 | 24 | Numbered releases: https://github.com/vgmstream/vgmstream/releases 25 | 26 | Help can be found here: https://www.hcs64.com/ 27 | 28 | More documentation: https://github.com/vgmstream/vgmstream/tree/master/doc 29 | 30 | ## Getting vgmstream 31 | There are multiple end-user components: 32 | - [vgmstream-cli](doc/USAGE.md#testexevgmstream-cli-command-line-decoder): A command-line decoder. 33 | - [in_vgmstream](doc/USAGE.md#in_vgmstream-winamp-plugin): A Winamp plugin. 34 | - [foo_input_vgmstream](doc/USAGE.md#foo_input_vgmstream-foobar2000-plugin): A foobar2000 component. 35 | - [xmp-vgmstream](doc/USAGE.md#xmp-vgmstream-xmplay-plugin): An XMPlay plugin. 36 | - [vgmstream.so](doc/USAGE.md#audacious-plugin): An Audacious plugin. 37 | - [vgmstream123](doc/USAGE.md#vgmstream123-command-line-player): A command-line player. 38 | 39 | The main library (plain *vgmstream*) is the code that handles the internal conversion, while the 40 | above components are what you use to get sound. 41 | 42 | ### Usage 43 | If you want to convert game audio to `.wav`, get *vgmstream-cli* then drag-and-drop one 44 | or more files to the executable (support may vary per O.S. or distro). This should create 45 | `(file.extension).wav`, if the format is supported. You can also try the online web player 46 | instead. See: https://vgmstream.org 47 | 48 | More user-friendly would be installing a player like *foobar2000* (on Windows) or *Audacious* 49 | (on Linux) and the vgmstream plugin. Then you can directly listen your files and set options like 50 | infinite looping, or convert to `.wav` with the player's options (also easier to use if your file 51 | has multiple "subsongs"). 52 | 53 | See [components](doc/USAGE.md#components) in the *usage guide* for full install instructions and 54 | explanations. The aim is feature parity, but there are a few differences between them due to 55 | missing parts on vgmstream's side or lack of support in the player. 56 | 57 | Note that vgmstream cannot *encode* (convert from `.wav` to a game format), it only *decodes* 58 | (plays game audio). 59 | 60 | ### Windows binaries 61 | Prebuilt binaries: 62 | - https://vgmstream.org (latest) 63 | - https://github.com/vgmstream/vgmstream/releases (infrequent numbered releases) 64 | 65 | The foobar2000 component is also available on https://www.foobar2000.org based on current 66 | release. 67 | 68 | You may also try the alternative versions (irregularly) built by [bnnm](https://github.com/bnnm): 69 | - https://github.com/bnnm/vgmstream-builds/raw/master/bin/vgmstream-latest-test-u.zip 70 | 71 | Or compile from source, see the [build guide](doc/BUILD.md). 72 | 73 | ### Linux binaries 74 | A prebuilt CLI binary is available. It's statically linked and should work on systems running 75 | Linux kernel v3.2 and above: 76 | - https://vgmstream.org (latest) 77 | - https://github.com/vgmstream/vgmstream/releases (infrequent numbered releases) 78 | 79 | Building from source will also give you *vgmstream.so* (Audacious plugin), and *vgmstream123* 80 | (command-line player), which can't be statically linked. 81 | 82 | When building it needs several external libraries. For a quick script for Debian and Ubuntu-style 83 | distros run `./make-build-cmake.sh`. The script will need to install dependencies first, so you 84 | may prefer to run steps manually, which the [build guide](doc/BUILD.md) describes in detail. 85 | 86 | ### macOS binaries 87 | A prebuilt CLI binary is available: 88 | - https://vgmstream.org (latest) 89 | - https://github.com/vgmstream/vgmstream/releases (infrequent numbered releases) 90 | 91 | Otherwise follow the [build guide](doc/BUILD.md). 92 | 93 | 94 | ## More info 95 | - [Usage guide](doc/USAGE.md) 96 | - [List of supported audio formats](doc/FORMATS.md) 97 | - [Build guide](doc/BUILD.md) 98 | - [TXTH file format](doc/TXTH.md) 99 | - [TXTP file format](doc/TXTP.md) 100 | 101 | 102 | Enjoy! *hcs* 103 | -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/avcodec-vgmstream-59.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/avcodec-vgmstream-59.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/avformat-vgmstream-59.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/avformat-vgmstream-59.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/avutil-vgmstream-57.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/avutil-vgmstream-57.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/libatrac9.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/libatrac9.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/libcelt-0061.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/libcelt-0061.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/libcelt-0110.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/libcelt-0110.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/libg719_decode.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/libg719_decode.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/libmpg123-0.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/libmpg123-0.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/libspeex-1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/libspeex-1.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/libvorbis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/libvorbis.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/swresample-vgmstream-4.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/swresample-vgmstream-4.dll -------------------------------------------------------------------------------- /Galgame/CIRCUS/vgmstream-win64/vgmstream-cli.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/CIRCUS/vgmstream-win64/vgmstream-cli.exe -------------------------------------------------------------------------------- /Galgame/Mirai/1.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import json 4 | import argparse 5 | from tqdm import tqdm 6 | from glob import glob 7 | 8 | def parse_args(args=None, namespace=None): 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument("-JA", type=str, default=r"D:\Fuck_galgame\script") 11 | parser.add_argument("-op", type=str, default=r'D:\Fuck_galgame\index.json') 12 | return parser.parse_args(args=args, namespace=namespace) 13 | 14 | def text_cleaning(text): 15 | text = re.sub(r'\[.*?\]', '', text) 16 | text = re.sub(r'\{([^/{}]+)/[^{}]+\}', r'\1', text) 17 | text = text.replace('『', '').replace('』', '').replace('「', '').replace('」', '').replace('(', '').replace(')', '') 18 | text = text.replace(' ', '').replace('/', '').replace('\u3000', '') 19 | return text 20 | 21 | def main(JA_dir, op_json): 22 | filelist = [] 23 | for path in glob(f"{JA_dir}/**/*", recursive=True): 24 | if os.path.isfile(path) and '.' not in os.path.basename(path): 25 | filelist.append(path) 26 | results = [] 27 | for filename in tqdm(filelist): 28 | with open(filename, 'r', encoding='cp932') as file: 29 | lines = file.readlines() 30 | 31 | for i, line in enumerate(lines): 32 | match = re.findall(r'【([^,]+),([^】]+)】(.+)', line) 33 | if match: 34 | match = match[0] 35 | Speaker = match[0] 36 | Speaker = Speaker.split('@')[0] 37 | Voice = match[1] 38 | Text = match[2] 39 | Text = text_cleaning(Text) 40 | results.append((Speaker, Voice, Text)) 41 | 42 | with open(op_json, mode='w', encoding='utf-8') as file: 43 | seen = set() 44 | json_data = [] 45 | for Speaker, Voice, Text in results: 46 | if Voice.lower() not in seen: 47 | seen.add(Voice.lower()) 48 | json_data.append({'Speaker': Speaker, 'Voice': Voice, 'Text': Text}) 49 | json.dump(json_data, file, ensure_ascii=False, indent=4) 50 | 51 | if __name__ == '__main__': 52 | args = parse_args() 53 | main(args.JA, args.op) -------------------------------------------------------------------------------- /Galgame/NeXAS/1.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import json 4 | import argparse 5 | from tqdm import tqdm 6 | from glob import glob 7 | from tool.message import text_analyze 8 | from tool.disassembler import Disassembler 9 | 10 | def parse_args(args=None, namespace=None): 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument("-JA", type=str, default=r"D:\Fuck_galgame\script") 13 | parser.add_argument("-op", type=str, default=r'D:\Fuck_galgame\index.json') 14 | parser.add_argument("-ft", type=float, default=1) 15 | return parser.parse_args(args=args, namespace=namespace) 16 | 17 | def text_cleaning(text): 18 | text = text.replace('『', '').replace('』', '').replace('「', '').replace('」', '').replace('(', '').replace(')', '').replace('〔', '').replace('〕', '').replace('《', '').replace('》', '') 19 | text = text.replace(' ', '') 20 | return text 21 | 22 | def get_text(text): 23 | Voice = Text = None 24 | match = re.match(r'\[\d+\](.*)', text) 25 | if match: 26 | result, display = text_analyze(match.group(1)) 27 | if result and display: 28 | for item in result: 29 | if item.escape.name == "VOICE": 30 | Voice = item.command 31 | Text = text_cleaning(display) 32 | 33 | return Voice, Text 34 | 35 | def main(JA_dir, op_json, force_type): 36 | filelist = glob(f"{JA_dir}/**/*.bin", recursive=True) 37 | filelist += glob(f"{JA_dir}/**/*.binu8", recursive=True) 38 | 39 | global_bin = None 40 | filtered_filelist = [] 41 | 42 | for f in filelist: 43 | if os.path.basename(f).lower() == "__global.bin" or os.path.basename(f).lower() == "__global.binu8": 44 | global_bin = f 45 | else: 46 | filtered_filelist.append(f) 47 | results = [] 48 | match force_type: 49 | case 0: 50 | disassembler = Disassembler(r"src\NeXAS\version_0.yaml") 51 | case 1: 52 | disassembler = Disassembler(r"src\NeXAS\version_1.yaml") 53 | case 2: 54 | disassembler = Disassembler(r"src\NeXAS\version_2.yaml") 55 | disassembler.load_and_disassemble_global(global_bin) 56 | for filename in tqdm(filtered_filelist): 57 | dis_result = disassembler.disassemble(filename) 58 | for label in dis_result['code']: 59 | for i, block in enumerate(label['instructions']): 60 | out_mnemonic = block['mnemonic'] 61 | out_operand = block.get('operand') 62 | out_param_type = block.get('param_type') 63 | out_comment = block.get('comment') 64 | 65 | if out_mnemonic and out_operand and out_param_type and out_comment: 66 | if out_mnemonic == "PARAM" and out_operand == 1 and out_param_type == "@STRING": 67 | Voice, Text = get_text(out_comment) 68 | if Voice and Text: 69 | if '@u' in Text or '@b' in Text or '@*name' in Text: 70 | continue 71 | if label['instructions'][i - 1]['mnemonic'] == "VAL" and label['instructions'][i - 1]['comment'] == "": 72 | if label['instructions'][i - 2]['mnemonic'] == "PARAM": 73 | Speaker = label['instructions'][i - 2]['comment'] 74 | match = re.match(r'\[\d+\](.*)', Speaker) 75 | if match: 76 | Speaker = match.group(1) 77 | Speaker = Speaker.replace(' ', '').replace(' ', '').replace('\t', '') 78 | if Speaker == "": 79 | Speaker = "???" 80 | Speaker = re.sub(r'@[0-9A-Za-z_-]*', '', Speaker) 81 | results.append((Speaker, Voice, Text)) 82 | 83 | with open(op_json, mode='w', encoding='utf-8') as file: 84 | seen = set() 85 | json_data = [] 86 | for Speaker, Voice, Text in results: 87 | if Voice.lower() not in seen: 88 | seen.add(Voice.lower()) 89 | json_data.append({'Speaker': Speaker, 'Voice': Voice, 'Text': Text}) 90 | json.dump(json_data, file, ensure_ascii=False, indent=4) 91 | 92 | if __name__ == '__main__': 93 | args = parse_args() 94 | main(args.JA, args.op, args.ft) -------------------------------------------------------------------------------- /Galgame/NeXAS/tool/constants.py: -------------------------------------------------------------------------------- 1 | FLAG_GLOBALVAR = 0x40000000 2 | FLAG_STRINGVAR = 0x80000000 3 | FLAG_FUNC = 0x8000 4 | 5 | NULL_TERMINATED = 1 6 | LENGTH_PREFIXED = 2 -------------------------------------------------------------------------------- /Galgame/NeXAS/tool/reader.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Any 2 | from ruamel.yaml.scalarstring import SingleQuotedScalarString 3 | import struct 4 | import io 5 | 6 | from .constants import * 7 | from .script_data import * 8 | 9 | class Reader: 10 | def __init__(self, config): 11 | self.data = io.BytesIO() 12 | self.config = config 13 | if self.config["string_format"] == NULL_TERMINATED: 14 | self.read_string = self._read_null_terminated_string 15 | elif self.config["string_format"] == LENGTH_PREFIXED: 16 | self.read_string = self._read_length_prefixed_string 17 | else: 18 | raise ValueError("Undefined string format") 19 | 20 | def _read_i32(self): 21 | return struct.unpack(' None: 41 | if self.config['version_format'] == LENGTH_PREFIXED: 42 | self.data.seek(13, 1) # 09 00 00 00 56 45 52 2D 31 2E 30 30 00 <= length + VER-1.00\0 + unknown unchanging data 43 | unk_count = self._read_i32() 44 | self.data.seek(unk_count * 4, 1) 45 | elif self.config['version_format'] == NULL_TERMINATED: 46 | self.data.seek(9, 1) # 56 45 52 2D 31 2E 30 30 00 <= VER-1.00\0 + unknown unchanging data 47 | unk_count = self._read_i32() 48 | self.data.seek(unk_count * 4, 1) 49 | 50 | def _read_instructions(self) -> List[Instruction]: 51 | code_count = self._read_i32() 52 | return [Instruction(*struct.unpack(' Dict[int, str]: 55 | length = self._read_i32() 56 | return {i: SingleQuotedScalarString(self.read_string()) for i in range(length)} 57 | 58 | def _read_banks_params(self) -> Dict[int, Any]: 59 | bank_count = self._read_i32() 60 | banks = {} 61 | for _ in range(bank_count): 62 | bank_no = self._read_i32() 63 | params = [{'int_var_index': self._read_i32()} for _ in range(8)] 64 | for i in range(8): 65 | params[i]['default_value'] = self._read_i32() 66 | banks[bank_no] = params 67 | return banks 68 | 69 | def _read_functions(self) -> Dict[int, Any]: 70 | functions = {} 71 | while True: 72 | key = self.data.read(4) 73 | if not key: 74 | break 75 | key = struct.unpack(' Script: 89 | with open(input, 'rb') as file: 90 | self.data = io.BytesIO(file.read()) 91 | 92 | self._read_version() 93 | init_code = self._read_instructions() 94 | code = self._read_instructions() 95 | string_table = self._read_string_list() 96 | int_var_names = self._read_string_list() 97 | string_var_names = self._read_string_list() 98 | banks_params = self._read_banks_params() 99 | functions = self._read_functions() 100 | 101 | return Script(init_code, code, string_table, int_var_names, string_var_names, banks_params, functions) 102 | 103 | def read_global_script_from_file(self, input: str) -> GlobalScript: 104 | with open(input, 'rb') as file: 105 | self.data = io.BytesIO(file.read()) 106 | 107 | int_var_names = self._read_string_list() 108 | string_var_names = self._read_string_list() 109 | string_table = self._read_string_list() 110 | code = self._read_instructions() 111 | 112 | return GlobalScript (int_var_names, string_var_names, string_table, code) -------------------------------------------------------------------------------- /Galgame/NeXAS/tool/script_data.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, List, Any 3 | 4 | @dataclass 5 | class Opcode: 6 | id: int 7 | name: str 8 | 9 | @dataclass 10 | class Instruction: 11 | opcode: int 12 | operand: int 13 | 14 | @dataclass 15 | class Marker: 16 | count: int = 0 17 | 18 | @dataclass 19 | class Script: 20 | init_code: List[Instruction] 21 | code: List[Instruction] 22 | string_table: Dict[int, str] 23 | int_var_names: Dict[int, str] 24 | string_var_names: Dict[int, str] 25 | banks_params: Dict[int, Any] 26 | functions: Dict[int, Any] 27 | 28 | @dataclass 29 | class GlobalScript: 30 | int_var_names: Dict[int, str] 31 | string_var_names: Dict[int, str] 32 | string_table: Dict[int, str] 33 | code: List[Instruction] -------------------------------------------------------------------------------- /Galgame/NeXAS_Unity/spm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class AlicePackageFile: 4 | class PFHeader: 5 | def __init__(self, file_name, read_start_byte_pos, byte_length): 6 | self.file_name = file_name 7 | self.read_start_byte_pos = read_start_byte_pos 8 | self.byte_length = byte_length 9 | 10 | def __repr__(self): 11 | return f"PFHeader(file_name={self.file_name!r}, read_start_byte_pos={self.read_start_byte_pos}, byte_length={self.byte_length})" 12 | 13 | def __init__(self, file_path): 14 | self.file_path = file_path 15 | self.header_dict = {} # Similar to Dictionary 16 | self.parsing_pack(file_path) 17 | 18 | def parsing_pack(self, file_path): 19 | # Determine total file size 20 | file_size = os.path.getsize(file_path) 21 | 22 | with open(file_path, "rb") as f: 23 | # Skip first 16 bytes (assumed to be a file header or metadata) 24 | f.seek(16, os.SEEK_SET) 25 | 26 | while f.tell() < file_size: 27 | # Read 32 bytes for the file name field (potentially null-terminated) 28 | name_bytes = f.read(32) 29 | if len(name_bytes) < 32: 30 | # Exit loop if we didn't get a full 32-byte record (could be corrupted or end-of-file) 31 | break 32 | 33 | # Find the first null byte (0) to determine the end of the actual file name 34 | null_index = name_bytes.find(b'\x00') 35 | if null_index == -1: 36 | # If no null terminator is found, use the full 32 bytes 37 | actual_name_bytes = name_bytes 38 | else: 39 | actual_name_bytes = name_bytes[:null_index] 40 | 41 | # Decode file name using UTF-8 encoding 42 | file_name = actual_name_bytes.decode("utf-8") 43 | 44 | # Read next 8 bytes for the starting byte (as a little-endian 64-bit integer) 45 | start_bytes = f.read(8) 46 | if len(start_bytes) < 8: 47 | break 48 | # Convert the bytes to an integer (assuming little-endian) 49 | start_int = int.from_bytes(start_bytes, byteorder="little", signed=False) 50 | 51 | # Read following 8 bytes for the byte length field 52 | length_bytes = f.read(8) 53 | if len(length_bytes) < 8: 54 | break 55 | byte_length = int.from_bytes(length_bytes, byteorder="little", signed=False) 56 | 57 | # Adjust the read start position by adding 16 (as done in the C# code) 58 | read_start_byte_pos = start_int + 16 59 | 60 | # Create a PFHeader object 61 | header = AlicePackageFile.PFHeader(file_name, read_start_byte_pos, byte_length) 62 | 63 | # Add header to the dictionary using file_name as key 64 | self.header_dict[file_name] = header 65 | 66 | # Skip forward by byte_length bytes (this moves the stream position ahead) 67 | f.seek(byte_length, os.SEEK_CUR) 68 | 69 | if __name__ == "__main__": 70 | file_path = r"D:\青夏轨迹v2 R18\SPM.pac" 71 | try: 72 | pack = AlicePackageFile(file_path) 73 | print("Parsed header dictionary:") 74 | for key, header in pack.header_dict.items(): 75 | print(key, ":", header) 76 | except Exception as e: 77 | print("Error reading package file:", e) -------------------------------------------------------------------------------- /Galgame/NekoSDK/1.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import argparse 4 | from glob import glob 5 | from tqdm import tqdm 6 | 7 | def parse_args(args=None, namespace=None): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("-JA", type=str, default=r"D:\Fuck_galgame\script") 10 | parser.add_argument("-op", type=str, default=r'D:\Fuck_galgame\index.json') 11 | return parser.parse_args(args=args, namespace=namespace) 12 | 13 | def text_cleaning(text): 14 | text = text.replace('『', '').replace('』', '').replace('「', '').replace('」', '').replace('(', '').replace(')', '') 15 | text = text.replace(' ', '').replace('\n', '').replace('\u3000', '') 16 | return text 17 | 18 | def main(JA_dir, op_json): 19 | filelist = glob(f"{JA_dir}/**/*.txt", recursive=True) 20 | results = [] 21 | for filename in tqdm(filelist): 22 | with open(filename, 'rb') as file: 23 | data = file.read() 24 | 25 | i = 0 26 | while i < len(data): 27 | if data[i: i + 14] == b'\x5B\x83\x65\x83\x4C\x83\x58\x83\x67\x95\x5C\x8E\xA6\x5D': # [テキスト表示] 28 | i += 14 29 | 30 | segment = bytearray() 31 | while i < len(data) and data[i: i + 2] != b'\x0A\x00': 32 | segment.append(data[i]) 33 | i += 1 34 | decoded_segment = segment.decode('cp932', errors='ignore') 35 | match = re.findall(r'^\s*(\S+)\s+voice\\([\w\d_]+\.ogg)\n(.+)$', decoded_segment, re.DOTALL) 36 | if match: 37 | match = match[0] 38 | Speaker = match[0] 39 | Voice = match[1].replace('.ogg', '') 40 | if 'may1_0008' in Voice: 41 | print(f"Error: {filename}, {i}") 42 | Text = match[2] 43 | Text = text_cleaning(Text) 44 | results.append((Speaker, Voice, Text)) 45 | else: 46 | i += 1 47 | 48 | with open(op_json, mode='w', encoding='utf-8') as file: 49 | seen = set() 50 | json_data = [] 51 | for Speaker, Voice, Text in results: 52 | if Voice not in seen: 53 | seen.add(Voice) 54 | json_data.append({'Speaker': Speaker, 'Voice': Voice, 'Text': Text}) 55 | json.dump(json_data, file, ensure_ascii=False, indent=4) 56 | 57 | if __name__ == '__main__': 58 | args = parse_args() 59 | main(args.JA, args.op) -------------------------------------------------------------------------------- /Galgame/README.md: -------------------------------------------------------------------------------- 1 | ## 本工具包默认用户已经知晓如何对热门游戏进行解包,但不知道如何处理加密脚本 2 | 3 | ### Calculater_Total_Duration.py 4 | 功能:计算任意文件夹下面,所有音频的总时长 5 | 6 | ### Combiner_Batch.py 7 | 功能:批量合并音频 8 | 9 | ### Combiner_To_6s.py 10 | 功能:低于6秒的短音频进行合并 11 | 12 | ### Deleter_Blank.py 13 | 功能:删除音频中的空白 14 | 15 | ### Deleter_Over_30s.py 16 | 功能:删除任意文件夹下面,超过30秒的音频 17 | 18 | ### Resampler.py 19 | 功能:批量重采样音频 20 | 21 | ### SAMI.py 22 | 功能:调用SAMI进行伴奏人声分离 23 | 24 | ### TextCleaner_ArtemisEngine_xx.py 25 | 适配的游戏:部分Artemis Engine脚本 26 | 27 | 前置工具:None 28 | 29 | ### TextCleaner_BGI_xx.py 30 | 适配的游戏:部分Buriko General Interpreter脚本 31 | 32 | 前置工具:None 33 | 34 | ### TextCleaner_CatSystem2.py 35 | 适配的游戏:所有CatSystem2脚本 36 | 37 | 前置工具:None 38 | 39 | ### TextCleaner_DATEALIVE.py 40 | 适配的游戏:https://store.steampowered.com/app/1047440/DATE_A_LIVE_Rio_Reincarnation/ 41 | 42 | 前置工具:https://github.com/thesupersonic16/DALTools 43 | 44 | ### TextCleaner_FAVORITE.py 45 | 适配的游戏:所有Favorite Viewpoint System脚本 46 | 47 | 前置工具:https://github.com/Tabing010102/fvp 48 | 49 | ### TextCleaner_Genshin_StarRail_CN.py 50 | 适配的游戏:原神4.2,崩坏:星穹铁道1.6(新版本原神会漏标识符) 51 | 52 | 前置数据:https://www.bilibili.com/read/cv24180458 53 | 54 | ### TextCleaner_Kirikiri_ks.py 55 | 适配的游戏:部kirikiri ks脚本(种类太多了,几乎每个会社都搞了一套) 56 | 57 | 前置工具:None 58 | 59 | ### TextCleaner_Kirikiri_scn.py 60 | 适配的游戏:所有kirikiri scn脚本 61 | 62 | 前置工具:https://github.com/UlyssesWu/FreeMote 63 | 64 | ### TextCleaner_MCSM.py(不可用,有时间会逆一下主程序,目前的脚本结构是猜的) 65 | 适配的游戏:我的世界故事模式第一季&第二季 66 | 67 | 前置工具:https://aluigi.altervista.org/papers/ttarchext.zip 68 | 69 | ### TextCleaner_SiglusEngine.py 70 | 适配的游戏:所有Siglus Engine脚本 71 | 72 | 前置工具:TextCleaner_SiglusEngine.7z -------------------------------------------------------------------------------- /Galgame/SiglusEngine/TextCleaner_SiglusEngine.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/SiglusEngine/TextCleaner_SiglusEngine.exe -------------------------------------------------------------------------------- /Galgame/SiglusEngine/TextCleaner_SiglusEngine.ini: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | E:\Games\Galgame\JADE\Love - Destination 5 | 6 | 1158,596,1402,843 7 | 8 | 9 | -------------------------------------------------------------------------------- /Galgame/SiglusEngine/TextCleaner_SiglusEngine.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import argparse 4 | from glob import glob 5 | 6 | def parse_args(args=None, namespace=None): 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument("-JA", type=str, default=r"D:\Fuck_galgame\sc") 9 | parser.add_argument("-op", type=str, default=r'D:\Fuck_galgame\index.json') 10 | return parser.parse_args(args=args, namespace=namespace) 11 | 12 | def speaker_cleaning(speaker_ori): 13 | speaker = speaker_ori.split('/')[0] 14 | speaker = speaker.replace('◎', '').replace('★', '') 15 | return speaker 16 | 17 | def text_cleaning(text): 18 | text = re.sub(r'ruby\(([^)]+)\)\s*(.+?)\s*ruby', r'\1', text) 19 | text = re.sub(r'\{[^:]*:([^}]*)\}', r'\1', text) 20 | text = re.sub(r'([^)]*$|@([a-zA-Z_]+)\((.*?)\)|『.*?』|', '', text) 21 | text = re.sub(r'[A-Za-z_]+\(\d+\)\s*', '', text) 22 | text = text.replace('" "', '').replace('nli', '').replace('NLI', '') 23 | text = text.replace('"', '').replace('『', '').replace('』', '').replace('〝', '').replace('〟', '').replace('ヽ ', '') 24 | text = text.replace(' ', '').replace('///', '').replace('@heart', '').replace('〈', '').replace('〉', '').replace('@', '') 25 | return text 26 | 27 | def main(JA_dir, op_json): 28 | filelist = glob(f"{JA_dir}/**/*.ss", recursive=True) 29 | results = [] 30 | seen_voices = set() 31 | 32 | for files in filelist: 33 | try: 34 | with open(files, 'r', encoding='cp932') as file: 35 | lines = file.readlines() 36 | except UnicodeDecodeError: 37 | with open(files, 'r', encoding='utf-8') as file: 38 | lines = file.readlines() 39 | 40 | lines = [re.sub(r'timewait\(\d+\)', '', line.strip()) for line in lines] 41 | print(files) 42 | 43 | for i, line in enumerate(lines): 44 | if line.startswith('//'): 45 | continue 46 | if 'multi_msg' in line or '@KOE' in line: 47 | continue 48 | if (match := re.search(r"KOE\((\d+),\d+\).*?【(.+?)】", line)): 49 | Voice = match.group(1) 50 | Voice = f"z{Voice[:4]}#{Voice[4:]}" 51 | Speaker = speaker_cleaning(match.group(2)) 52 | 53 | # 只在剩下的字符串中搜索文本内容 54 | rest_line = line[match.end():] 55 | if (text_match := re.search(r"[「『(](.+?)[」』)]", rest_line)): 56 | Text = text_match.group(1) 57 | try: 58 | Text = text_cleaning(Text) 59 | except: 60 | continue 61 | if "(" in Text: # 摆烂了,屎太多了,懒得清理了 62 | continue 63 | if Voice in seen_voices: 64 | print(f"重复的 Voice: {Voice}, Speaker: {Speaker}, Text: {Text}") 65 | else: 66 | seen_voices.add(Voice) 67 | results.append((Speaker, Voice, Text)) 68 | 69 | with open(op_json, mode='w', encoding='utf-8') as file: 70 | json_data = [{'Speaker': Speaker, 'Voice': Voice, 'Text': Text} for Speaker, Voice, Text in results] 71 | json.dump(json_data, file, ensure_ascii=False, indent=4) 72 | 73 | if __name__ == '__main__': 74 | args = parse_args() 75 | main(args.JA, args.op) -------------------------------------------------------------------------------- /Galgame/SiglusEngine/TextCleaner_SiglusEngine_File.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import shutil 4 | from tqdm import tqdm 5 | 6 | input_json_filepath = r'D:\Fuck_galgame\index.json' 7 | source_folder = r"E:\Games\Galgame\JADE\Love - Destination\__Unpack__\OVK" 8 | output_folder = r"D:\Fuck_galgame\JADE_Love - Destination" 9 | 10 | with open(input_json_filepath, 'r', encoding='utf-8') as file: 11 | dialogues = json.load(file) 12 | 13 | # 构建文件映射 14 | file_mapping = {} 15 | for root, _, files in os.walk(source_folder): 16 | for filename in files: 17 | name, ext = os.path.splitext(filename) 18 | folder_name = os.path.basename(root) 19 | if ext.lower() == '.ogg': # 假设我们只关心 .ogg 文件 20 | key = f"{folder_name.lower()}#{int(name):05d}" 21 | file_mapping[key] = os.path.join(root, filename) 22 | 23 | if not os.path.exists(output_folder): 24 | os.makedirs(output_folder) 25 | 26 | output_data = [] 27 | for dialogue in tqdm(dialogues): 28 | voice = dialogue['Voice'] 29 | folder_name, file_id = voice.split('#') 30 | file_id = f"{int(file_id):05d}" # 将文件 ID 转换为整数并格式化为 5 位数 31 | key = f"{folder_name.lower()}#{file_id}" 32 | 33 | if key in file_mapping: 34 | src_path = file_mapping[key] 35 | 36 | # 创建以 Speaker 命名的子文件夹 37 | speaker_folder = os.path.join(output_folder, dialogue['Speaker']) 38 | if not os.path.exists(speaker_folder): 39 | os.makedirs(speaker_folder) 40 | 41 | relative_path = os.path.join(output_folder, dialogue['Speaker'], f"{key}.ogg") 42 | shutil.move(src_path, relative_path) 43 | 44 | output_data.append({ 45 | 'Speaker': dialogue['Speaker'], 46 | 'Text': dialogue['Text'], 47 | 'Voice': key 48 | }) 49 | 50 | output_json_filepath = os.path.join(output_folder, 'index.json') 51 | with open(output_json_filepath, 'w', encoding='utf-8') as file: 52 | json.dump(output_data, file, ensure_ascii=False, indent=4) -------------------------------------------------------------------------------- /Galgame/TextCleaner_BGI_C.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | ver000 = { 4 | 'HDR_SIZE' : 0x0, # base header size 5 | 'HDRAS_POS': None, # offset of additional header data size (set to None if not used) 6 | 7 | 'STR_TYPE': 0x3, # string type identifier 8 | 'FILE_TYPE': 0x7F, # file type identifier 9 | 10 | 'TEXT_FCN': 0x140, # function id for text command (set to None if not used) 11 | 'BKLG_FCN': 0x143, # function id for backlog text command (set to None if not used) 12 | 'RUBY_FCN': 0x14B, # function id for ruby command (set to None if not used) 13 | 14 | 'NAME_POS': 0x24, # offset of TXT_FCN from name argument 15 | 'TEXT_POS': 0x2C, # offset of TXT_FCN from text argument 16 | 'RUBYK_POS': 0x14, # offset of RUBY_FCN from kanji argument 17 | 'RUBYF_POS': 0x0C, # offset or RUBY_FCN from furigana argument 18 | 'BKLG_POS': 0x0C, # offset of BKLG_FCN from text argument 19 | } 20 | 21 | # header beginning with "BurikoCompiledScriptVer1.00" 22 | ver100 = { 23 | 'HDR_SIZE': 0x1C, # base header size 24 | 'HDRAS_POS': 0x1C, # offset of additional header data size (set to None if not used) 25 | 26 | 'STR_TYPE': 0x3, # string type identifier 27 | 'FILE_TYPE': 0x7F, # file type identifier 28 | 29 | 'TEXT_FCN': 0x140, # function id for text command (set to None if not used) 30 | 'BKLG_FCN': 0x143, # function id for backlog text command (set to None if not used) 31 | 'RUBY_FCN': 0x14B, # function id for ruby command (set to None if not used) 32 | 33 | 'NAME_POS': 0x0C, # offset of TXT_FCN from name argument 34 | 'TEXT_POS': 0x04, # offset of TXT_FCN from text argument 35 | 'RUBYK_POS': 0x04, # offset of RUBY_FCN from kanji argument 36 | 'RUBYF_POS': 0x0C, # offset or RUBY_FCN from furigana argument 37 | 'BKLG_POS': 0x0C, # offset of BKLG_FCN from text argument 38 | } 39 | 40 | # select which version based on known header string 41 | def get_config(data): 42 | if data.startswith(b'BurikoCompiledScriptVer1.00\x00'): 43 | config = ver100 44 | else: 45 | config = ver000 46 | return config 47 | 48 | def get_dword(data, offset): 49 | bytes = data[offset:offset + 4] 50 | if len(bytes) < 4: 51 | return None 52 | return struct.unpack(' 2 | 3 | 4 | E:\Games\Galgame\Hadashi Shoujo\Harem x Shangri-La 5 | 6 | 1158,596,1402,843 7 | 8 | 9 | -------------------------------------------------------------------------------- /Galgame/TextCleaner_Will Co.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import argparse 4 | from glob import glob 5 | from tqdm import tqdm 6 | 7 | def parse_args(args=None, namespace=None): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("-JA", type=str, default=r"D:\Fuck_galgame\sc") 10 | parser.add_argument("-op", type=str, default=r'D:\Fuck_galgame\index.json') 11 | return parser.parse_args(args=args, namespace=namespace) 12 | 13 | def text_cleaning(text): 14 | text = text.replace('『', '').replace('』', '').replace('「', '').replace('」', '').replace('(', '').replace(')', '') 15 | text = text.replace(' ', '').replace('\\n', '') 16 | return text 17 | 18 | def main(JA_dir, op_json): 19 | filelist = glob(f"{JA_dir}/**/*.wsc", recursive=True) 20 | speaker_start = False 21 | text_start = False 22 | results = [] 23 | for filename in tqdm(filelist): 24 | with open(filename, 'rb') as file: 25 | data = file.read() 26 | 27 | i = 0 28 | while i < len(data): 29 | if data[i: i + 2] == b'\x65\x00' and all(data[i + 2 + a] != 0x00 for a in range(3)): 30 | i += 2 31 | segment = bytearray() 32 | while i < len(data) and data[i] != 0: 33 | segment.append(data[i]) 34 | i += 1 35 | Voice = segment.decode('cp932') 36 | if Voice == '': 37 | pass 38 | Speaker_id = Voice.split('_')[0] 39 | speaker_start = True 40 | 41 | if data[i: i + 2] == b'\x0F\x0F' and speaker_start: 42 | i += 2 43 | segment = bytearray() 44 | while i < len(data) and data[i] != 0: 45 | segment.append(data[i]) 46 | i += 1 47 | Speaker = segment.decode('cp932') 48 | Speaker = Speaker.split('/')[-1] 49 | speaker_start = False 50 | text_start = True 51 | 52 | if data[i: i + 1] == b'\x00' and text_start: 53 | i += 1 54 | segment = bytearray() 55 | while i < len(data) and data[i: i + 1] != b'%': 56 | segment.append(data[i]) 57 | i += 1 58 | Text = segment.decode('cp932') 59 | Text = text_cleaning(Text) 60 | results.append((Speaker, Speaker_id, Voice, Text)) 61 | text_start = False 62 | 63 | else: 64 | i += 1 65 | 66 | replace_dict = {} 67 | for Speaker, Speaker_id, Voice, Text in tqdm(results): 68 | if Speaker != '???' and Speaker_id not in replace_dict: 69 | replace_dict[Speaker_id] = Speaker 70 | 71 | fixed_results = [] 72 | for Speaker, Speaker_id, Voice, Text in tqdm(results): 73 | if Speaker == '???' and Speaker_id in replace_dict: 74 | fixed_results.append((replace_dict[Speaker_id], Speaker_id, Voice, Text)) 75 | else: 76 | fixed_results.append((Speaker, Speaker_id, Voice, Text)) 77 | 78 | with open(op_json, mode='w', encoding='utf-8') as file: 79 | seen = set() 80 | json_data = [] 81 | for Speaker, Speaker_id, Voice, Text in fixed_results: 82 | if Voice not in seen: 83 | seen.add(Voice) 84 | json_data.append({'Speaker': Speaker, 'Voice': Voice, 'Text': Text}) 85 | json.dump(json_data, file, ensure_ascii=False, indent=4) 86 | 87 | if __name__ == '__main__': 88 | args = parse_args() 89 | main(args.JA, args.op) -------------------------------------------------------------------------------- /Galgame/TextCleaner_alice.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import argparse 4 | 5 | def parse_args(args=None, namespace=None): 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument("-JA", type=str, default=r"E:\Dataset\FuckGalGame\Alice Soft\Rance 03 - Leazas Kanraku\script.txt") 8 | parser.add_argument("-op", type=str, default=r'D:\AI\Audio_Tools\python\1.json') 9 | parser.add_argument("-ft", type=str, default=0) 10 | return parser.parse_args(args=args, namespace=namespace) 11 | 12 | def text_cleaning(text): 13 | text = re.sub(r'\[.*?\]|\([^()]*\)', '', text) 14 | text = text.replace('『', '').replace('』', '').replace('「', '').replace('」', '').replace('(', '').replace(')', '') 15 | text = text.replace(' ', '').replace('/', '') 16 | return text 17 | 18 | def main(JA_file, op_json, force_type): 19 | with open(JA_file, 'r', encoding='utf-8') as file: 20 | lines = file.readlines() 21 | lines = [line.strip() for line in lines if not line.strip().startswith("PUSH") and not line.strip().startswith("CALLFUNC ■")] 22 | 23 | results = [] 24 | for i, line in enumerate(lines): 25 | if line == "CALLFUNC VOICE": 26 | if force_type == 0: 27 | Speaker = re.findall(r'S_PUSH\s*"([^"]+)"', lines[i - 4]) 28 | if not Speaker: 29 | continue 30 | Speaker = Speaker[0].split('/')[0] 31 | if force_type == 1: 32 | Speaker = re.findall(r'S_PUSH\s*"([^"]+)"', lines[i - 4]) 33 | if not Speaker: 34 | continue 35 | Speaker = Speaker[0].split('/')[1] 36 | Voice = re.findall(r'S_PUSH\s*"([^"]+)"', lines[i - 1])[0] 37 | 38 | Text = "" 39 | for j in range(i + 1, len(lines)): 40 | if lines[j] == 'RETURN': 41 | break 42 | elif lines[j] == 'CALLFUNC A': 43 | results.append((Speaker, Voice, Text)) 44 | break 45 | elif lines[j] == 'CALLFUNC R': 46 | continue 47 | elif lines[j].startswith('MSG'): 48 | sub_text = re.findall(r'"([^"]*)"', lines[j])[0] 49 | Text += text_cleaning(sub_text) 50 | 51 | with open(op_json, mode='w', encoding='utf-8') as file: 52 | seen = set() 53 | json_data = [] 54 | for Speaker, Voice, Text in results: 55 | record = (Speaker, Voice, Text) 56 | if record not in seen: 57 | seen.add(record) 58 | json_data.append({'Speaker': Speaker, 'Voice': Voice, 'Text': Text}) 59 | json.dump(json_data, file, ensure_ascii=False, indent=4) 60 | 61 | if __name__ == '__main__': 62 | args = parse_args() 63 | main(args.JA, args.op, args.ft) -------------------------------------------------------------------------------- /Galgame/TextCleaner_minori.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import argparse 4 | from tqdm import tqdm 5 | from glob import glob 6 | 7 | def parse_args(args=None, namespace=None): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("-JA", type=str, default=r"E:\Dataset\FuckGalGame\minori\Tsumi no Hikari Rendezvous Mikan Blossom\script") 10 | parser.add_argument("-op", type=str, default=r'D:\AI\Audio_Tools\python\1.json') 11 | return parser.parse_args(args=args, namespace=namespace) 12 | 13 | def text_cleaning(text): 14 | text = re.sub(r"\\[a-zA-Z]", '', text) 15 | text = text.replace('『', '').replace('』', '').replace('「', '').replace('」', '').replace('(', '').replace(')', '') 16 | return text 17 | 18 | def message(line): 19 | match1 = re.match(r"\.message\s([^ ]+)\s([^ ]+)\s([^ ]+)\s([^ ]+)", line, re.DOTALL) 20 | if match1: 21 | voice = match1.group(2) 22 | voice = re.sub(r"\[.*?\]", '', voice) 23 | speaker = match1.group(3) 24 | text = match1.group(4).strip() 25 | return speaker, voice, text 26 | else: 27 | match2 = re.match(r"\.message (.+) (.+)\s{2,}(.+)", line, re.DOTALL) 28 | if match2: 29 | voice = match2.group(2).strip() 30 | voice = re.sub(r"\[.*?\]", '', voice) 31 | speaker = '旁白' 32 | text = match2.group(3).strip() 33 | return speaker, voice, text 34 | else: 35 | return None 36 | 37 | def main(JA_dir, op_json): 38 | filelist = glob(f"{JA_dir}/**/*.sc", recursive=True) 39 | results = [] 40 | for filename in tqdm(filelist): 41 | with open(filename, 'r', encoding='cp932') as file: 42 | lines = file.readlines() 43 | lines = [re.sub(r'\{.*?\}', '', line.strip()).replace('\\n', '').replace('#', '').replace(' ', '').replace('@', '') for line in lines] 44 | for i, line in enumerate(lines): 45 | details = message(line) 46 | if details: 47 | speaker, voice, text = details 48 | text = text_cleaning(text) 49 | if not text: 50 | continue 51 | results.append((speaker, voice, text)) 52 | 53 | with open(op_json, mode='w', encoding='utf-8') as file: 54 | seen = set() 55 | json_data = [] 56 | for Speaker, Voice, Text in results: 57 | record = (Speaker, Voice, Text) 58 | if record not in seen: 59 | seen.add(record) 60 | json_data.append({'Speaker': Speaker, 'Voice': Voice, 'Text': Text}) 61 | json.dump(json_data, file, ensure_ascii=False, indent=4) 62 | 63 | if __name__ == '__main__': 64 | args = parse_args() 65 | main(args.JA, args.op) -------------------------------------------------------------------------------- /Galgame/Text_Cleaner_Stack.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import argparse 4 | from tqdm import tqdm 5 | from glob import glob 6 | 7 | def parse_args(args=None, namespace=None): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("-JA", type=str, default=r"D:\Fuck_galgame\sc") 10 | parser.add_argument("-op", type=str, default=r'D:\Fuck_galgame\index.json') 11 | return parser.parse_args(args=args, namespace=namespace) 12 | 13 | def text_cleaning(text): 14 | text = text.replace('\\n', '').replace('\n', '').replace(' ', '') 15 | return text 16 | 17 | def main(JA_dir, op_json): 18 | filelist = glob(f"{JA_dir}/**/*.ORS", recursive=True) 19 | results = [] 20 | for filename in tqdm(filelist): 21 | with open(filename, 'r', encoding='cp932', errors='replace') as file: 22 | lines = file.readlines() 23 | 24 | lines = [line.strip() for line in lines if line.strip() != ''] 25 | 26 | for i, line in enumerate(lines): 27 | if i <= len(lines) - 2 and line.startswith('[PrintText]') and lines[i + 1].startswith('[PlayVoice]'): 28 | match = re.match(r'\[PrintText\]="[^"]*?(\d{2}:\d{2}:\d{2}),\s*([^,]+?),\s*([^,]+?),\s*(\d{2}:\d{2}:\d{2})";', line) 29 | Speaker = match.group(2) 30 | Text = text_cleaning(match.group(3)) 31 | match = re.match(r'\[PlayVoice\]="[^"]*?(\d{2}:\d{2}:\d{2}),\s*([^,]+?),\s*([^,]+?),\s*([^,]+?),\s*(\d{2}:\d{2}:\d{2})";', lines[i + 1]) 32 | Voice = match.group(2) 33 | Voice = Voice.split('/')[-1] 34 | results.append((Speaker, Voice, Text)) 35 | 36 | with open(op_json, mode='w', encoding='utf-8') as file: 37 | seen = set() 38 | json_data = [] 39 | for Speaker, Voice, Text in results: 40 | if Voice.lower() not in seen: 41 | seen.add(Voice.lower()) 42 | json_data.append({'Speaker': Speaker, 'Voice': Voice, 'Text': Text}) 43 | json.dump(json_data, file, ensure_ascii=False, indent=4) 44 | 45 | if __name__ == '__main__': 46 | args = parse_args() 47 | main(args.JA, args.op) -------------------------------------------------------------------------------- /Galgame/YuRis/TextCleaner_YuRis_YSCM.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from typing import List 3 | 4 | class YSCM_Header_V5: 5 | def __init__(self, signature: bytes = b'', version: int = 0, command_count: int = 0, unknown0: int = 0): 6 | self.signature = signature # 4 bytes 7 | self.version = version # 4 bytes (uint32) 8 | self.command_count = command_count # 4 bytes (uint32) 9 | self.unknown0 = unknown0 # 4 bytes (uint32) 10 | 11 | def __repr__(self): 12 | return (f"") 13 | 14 | class YSCM_Arg_V5: 15 | def __init__(self, arg_name: str = '', value0: int = 0, value1: int = 0): 16 | self.arg_name = arg_name 17 | self.value0 = value0 18 | self.value1 = value1 19 | 20 | def __repr__(self): 21 | return f"" 22 | 23 | @property 24 | def arg_size(self) -> int: 25 | """返回参数在文件中的总字节大小 = (arg_name + '\0') + 1字节 + 1字节 = len + 3.""" 26 | return len(self.arg_name) + 3 27 | 28 | def to_dict(self, arg_id: int): 29 | return { 30 | "ID": f"0x{arg_id:x}", 31 | "Arg": self.arg_name, 32 | "Value0": f"0x{self.value0:x}", 33 | "Value1": f"0x{self.value1:x}" 34 | } 35 | 36 | class YSCM_Command_V5: 37 | def __init__(self, opcode: int = 0, command_name: str = '', args: List[YSCM_Arg_V5] = None): 38 | self.opcode = opcode 39 | self.command_name = command_name 40 | self.args = args or [] 41 | 42 | def __repr__(self): 43 | return (f"") 44 | 45 | @property 46 | def command_size(self) -> int: 47 | """返回指令在文件中的总字节大小。= (command_name + '\0') + 1字节(参数数量) + 所有参数大小之和.""" 48 | size = len(self.command_name) + 1 # command_name + '\0' 49 | size += 1 # arg_count 50 | for arg in self.args: 51 | size += arg.arg_size 52 | return size 53 | 54 | def to_dict(self): 55 | return { 56 | "OP": f"0x{self.opcode:x}", 57 | "Command": self.command_name, 58 | "Args": [arg.to_dict(i) for i, arg in enumerate(self.args)] 59 | } 60 | 61 | class YSCM_V5: 62 | def __init__(self): 63 | self.header = YSCM_Header_V5() 64 | self.commands: List[YSCM_Command_V5] = [] 65 | self.error_msgs: List[str] = [] 66 | self.unknow_table: bytes = b'' 67 | 68 | def load_file(self, filepath: str): 69 | with open(filepath, 'rb') as f: 70 | data = f.read() 71 | 72 | header_fmt = "<4sIII" 73 | header_size = struct.calcsize(header_fmt) 74 | sig, ver, cmd_count, unk0 = struct.unpack_from(header_fmt, data, 0) 75 | 76 | self.header = YSCM_Header_V5(signature=sig, version=ver, command_count=cmd_count, unknown0=unk0) 77 | 78 | offset = header_size 79 | 80 | # ------------ 2) 解析指令 ------------ 81 | self.commands.clear() 82 | for i in range(self.header.command_count): 83 | opcode = i 84 | cmd_offset = offset 85 | 86 | # 先读 command_name (零结尾字符串) 87 | cmd_name = _read_cstring(data, cmd_offset) 88 | cmd_offset += len(cmd_name) + 1 89 | 90 | # 再读参数数量 (1 byte) 91 | arg_count = data[cmd_offset] 92 | cmd_offset += 1 93 | 94 | args_list = [] 95 | for _ in range(arg_count): 96 | arg_offset = cmd_offset 97 | # 参数名 98 | arg_name = _read_cstring(data, arg_offset) 99 | arg_offset += len(arg_name) + 1 100 | 101 | # value0, value1 102 | value0 = data[arg_offset] 103 | value1 = data[arg_offset + 1] 104 | arg_offset += 2 105 | 106 | args_list.append(YSCM_Arg_V5(arg_name, value0, value1)) 107 | 108 | cmd_offset = arg_offset 109 | 110 | command_obj = YSCM_Command_V5(opcode, cmd_name, args_list) 111 | self.commands.append(command_obj) 112 | 113 | offset += command_obj.command_size 114 | 115 | self.error_msgs.clear() 116 | for _ in range(0x25): # 0x24+1 次 117 | msg = _read_cstring(data, offset) 118 | self.error_msgs.append(msg) 119 | offset += len(msg) + 1 120 | 121 | self.unknow_table = data[offset: offset + 0x100] 122 | offset += 0x100 123 | 124 | def to_json_str(self): 125 | output_dict = {"Commands": [cmd.to_dict() for cmd in self.commands]} 126 | # output_dict["ErrorMsgs"] = self.error_msgs 127 | # output_dict["UnknowTableHex"] = self.unknow_table.hex() 128 | 129 | return output_dict 130 | 131 | def _read_cstring(data: bytes, start_offset: int) -> str: 132 | """ 133 | 从 data[start_offset] 开始读取以 '\0' 结尾的字符串,返回字符串内容。 134 | """ 135 | end = start_offset 136 | while end < len(data) and data[end] != 0: 137 | end += 1 138 | return data[start_offset:end].decode('cp932', errors='replace') 139 | 140 | def YSCM(ysc_bin_path: str): 141 | parser = YSCM_V5() 142 | parser.load_file(ysc_bin_path) 143 | 144 | data = parser.to_json_str() 145 | 146 | for command in data["Commands"]: 147 | if command.get("Command") == "GOSUB": 148 | op_value = command.get("OP") 149 | print(f"Found Command: GOSUB with OP: {op_value}") 150 | 151 | for arg in command.get("Args", []): 152 | if arg.get("Arg") == "PSTR2": 153 | id_value = arg.get("ID") 154 | print(f"Found Arg: PSTR2 with ID: {id_value}") 155 | return int(op_value, 16), int(id_value, 16) -------------------------------------------------------------------------------- /Galgame/YuRis/TextCleaner_YuRis_YSTL.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | class YSTLHeader: 4 | def __init__(self, signature: str, version: int, entry_count: int): 5 | self.signature = signature 6 | self.version = version 7 | self.entry_count = entry_count 8 | 9 | def __repr__(self): 10 | return f"" 11 | 12 | class YSTLEntryV5: 13 | def __init__( 14 | self, 15 | uiSequence=0, 16 | uiPathSize=0, 17 | ucPathStr=b"", 18 | uiHighDateTime=0, 19 | uiLowDateTime=0, 20 | uiVariableCount=0, 21 | uiLabelCount=0, 22 | uiTextCount=0 23 | ): 24 | self.uiSequence = uiSequence 25 | self.uiPathSize = uiPathSize 26 | self.ucPathStr = ucPathStr 27 | self.uiHighDateTime = uiHighDateTime 28 | self.uiLowDateTime = uiLowDateTime 29 | self.uiVariableCount = uiVariableCount 30 | self.uiLabelCount = uiLabelCount 31 | self.uiTextCount = uiTextCount 32 | 33 | def get_path_str(self, encoding='cp932', slash='/'): 34 | decoded = self.ucPathStr.decode(encoding, errors='replace') 35 | decoded = decoded.replace('\\', slash) 36 | return decoded 37 | 38 | def __repr__(self): 39 | return (f"") 40 | 41 | def parse_ystl_v5(file_path: str): 42 | entries = [] 43 | 44 | with open(file_path, 'rb') as f: 45 | data = f.read(4 + 4 + 4) 46 | if len(data) < 12: 47 | raise ValueError("文件头不足 12 字节,非合法 YSTL") 48 | 49 | sig_bytes, version, entry_count = struct.unpack('<4sII', data) 50 | signature = sig_bytes.decode('ascii', errors='replace') 51 | 52 | if signature != "YSTL": 53 | raise ValueError(f"Signature 不正确: {signature}") 54 | 55 | header = YSTLHeader(signature, version, entry_count) 56 | 57 | for i in range(entry_count): 58 | uiSequence = struct.unpack(' 0: 83 | path_str = f"yst{entry.uiSequence:05d}.ybn" 84 | filelist.append(path_str) 85 | return filelist -------------------------------------------------------------------------------- /Galgame/krkr/TextCleaner_Kirikiri_scn_Psb.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/TextCleaner_Kirikiri_scn_Psb.exe -------------------------------------------------------------------------------- /Galgame/krkr/TextCleaner_Kirikiri_scn_Psb.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Galgame/krkr/TextCleaner_Kirikiri_scn_Psb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import threading 4 | 5 | print_lock = threading.Lock() 6 | 7 | def main(file_path): 8 | psb_decompile_path = os.path.join(os.path.dirname(__file__), 'TextCleaner_Kirikiri_scn_Psb.exe') 9 | command = [psb_decompile_path, file_path] 10 | subprocess.run(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 11 | 12 | def process_files(file_list): 13 | for file_path in file_list: 14 | main(file_path) 15 | 16 | if __name__ == '__main__': 17 | input_directory_path = r"D:\Fuck_galgame\script" 18 | THREAD_NUM = 20 19 | 20 | scn_files = [] 21 | for root, _, files in os.walk(input_directory_path): 22 | for file in files: 23 | file_path = os.path.join(root, file) 24 | scn_files.append(file_path) 25 | 26 | actual_threads = min(THREAD_NUM, len(scn_files)) 27 | 28 | chunks = [scn_files[i::actual_threads] for i in range(actual_threads)] 29 | chunks = [chunk for chunk in chunks if chunk] 30 | 31 | threads = [] 32 | for chunk in chunks: 33 | thread = threading.Thread(target=process_files, args=(chunk,)) 34 | threads.append(thread) 35 | thread.start() 36 | 37 | for thread in threads: 38 | thread.join() -------------------------------------------------------------------------------- /Galgame/krkr/TextCleaner_Kirikiri_scnname.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import argparse 4 | import shutil 5 | 6 | def parse_args(args=None, namespace=None): 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument("-JA", type=str, default=r"E:\Games\Galgame\Yuzusoft\Tenshi☆Souzou RE-BOOT!\Extractor_Output\data") 9 | parser.add_argument("-op", type=str, default=r"D:\Fuck_galgame\script") 10 | return parser.parse_args(args=args, namespace=namespace) 11 | 12 | def read_json_file(filepath): 13 | with open(filepath, 'r', encoding='utf-8') as file: 14 | return json.load(file) 15 | 16 | def main(JA_dir, op_dir): 17 | files_original = [] 18 | for root, dirs, files in os.walk(JA_dir): 19 | for f in files: 20 | if f.endswith('.json') and not f.endswith('.resx.json'): 21 | files_original.append(os.path.join(root, f)) 22 | 23 | for filepath in files_original: 24 | # 直接用 filepath,无需再拼接 JA_dir 25 | _0_JA_data = read_json_file(filepath) 26 | 27 | if 'scenes' in _0_JA_data and 'name' in _0_JA_data: 28 | json_name = _0_JA_data['name'] 29 | os.makedirs(op_dir, exist_ok=True) 30 | # 构造新文件名,目标目录为 op_dir 31 | new_file_path = os.path.join(op_dir, json_name + ".json") 32 | try: 33 | shutil.move(filepath, new_file_path) 34 | print(f"Renamed: {filepath} -> {new_file_path}") 35 | except Exception as e: 36 | print(f"Failed to rename {filepath}: {e}") 37 | 38 | if __name__ == '__main__': 39 | cmd = parse_args() 40 | main(cmd.JA, cmd.op) -------------------------------------------------------------------------------- /Galgame/krkr/krkr_cxdec_namestore.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | def main(): 5 | src_folder = r"E:\Games\Galgame\Lump of Sugar\Rurizakura\Extractor_Output" 6 | dst_folder = r"D:\Fuck_galgame\voice" 7 | 8 | # 如果目标文件夹不存在,则创建 9 | if not os.path.exists(dst_folder): 10 | os.makedirs(dst_folder) 11 | print(f"已创建目标文件夹: {dst_folder}") 12 | 13 | # 1. 递归扫描源文件夹及其子文件夹中所有没有后缀名的文件(名称为 hash 值) 14 | # 将文件名和完整路径存入字典 listA 15 | listA = {} 16 | for root, dirs, files in os.walk(src_folder): 17 | for file in files: 18 | file_path = os.path.join(root, file) 19 | name, ext = os.path.splitext(file) 20 | if ext == "": 21 | listA[file] = file_path 22 | 23 | print(f"在源文件夹及子文件夹中共找到 {len(listA)} 个没有后缀名的文件。") 24 | 25 | # 2. 读取 files_match.txt 文件,建立 hash -> 原始文件名 的映射 26 | mapping = {} # 键:hash 值;值:对应的原始文件名(包含扩展名) 27 | try: 28 | with open(r"D:\Fuck_galgame\files_match.txt", "r", encoding="utf-16-le") as f: 29 | for line in f: 30 | line = line.strip() 31 | if not line: 32 | continue 33 | # 每行格式: 文件名,hash值 34 | parts = line.split(',') 35 | if len(parts) != 2: 36 | print(f"跳过格式不对的行: {line}") 37 | continue 38 | original_filename = parts[0].strip() 39 | hash_value = parts[1].strip() 40 | mapping[hash_value] = original_filename 41 | except FileNotFoundError: 42 | print("未找到 files_match.txt 文件,请确认文件位置!") 43 | return 44 | 45 | print(f"从 files_match.txt 中读取到 {len(mapping)} 条匹配信息。") 46 | 47 | # 3. 根据 mapping 遍历每个 hash 值,在 listA 中查找对应的文件,如果存在,则复制并重命名到目标文件夹 48 | copied_count = 0 49 | for hash_value, original_filename in mapping.items(): 50 | if hash_value in listA: 51 | src_file_path = listA[hash_value] 52 | dst_file_path = os.path.join(dst_folder, original_filename) 53 | try: 54 | shutil.copy2(src_file_path, dst_file_path) 55 | copied_count += 1 56 | except Exception as e: 57 | print(f"复制 {hash_value} 时发生错误: {e}") 58 | 59 | print(f"共复制并重命名了 {copied_count} 个文件到目标文件夹:{dst_folder}") 60 | 61 | if __name__ == "__main__": 62 | main() -------------------------------------------------------------------------------- /Galgame/krkr/lib/BCnEncoder.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/BCnEncoder.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/Be.IO.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/Be.IO.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FastBitmapLib.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FastBitmapLib.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FreeMote.FastLz.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FreeMote.FastLz.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FreeMote.NET.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FreeMote.NET.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FreeMote.Plugins.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FreeMote.Plugins.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FreeMote.Plugins.x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FreeMote.Plugins.x64.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FreeMote.PsBuild.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FreeMote.PsBuild.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FreeMote.Psb.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FreeMote.Psb.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/FreeMote.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/FreeMote.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/K4os.Compression.LZ4.Streams.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/K4os.Compression.LZ4.Streams.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/K4os.Compression.LZ4.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/K4os.Compression.LZ4.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/K4os.Hash.xxHash.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/K4os.Hash.xxHash.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/McMaster.Extensions.CommandLineUtils.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/McMaster.Extensions.CommandLineUtils.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/Microsoft.Bcl.HashCode.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/Microsoft.Bcl.HashCode.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/Microsoft.IO.RecyclableMemoryStream.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/Microsoft.IO.RecyclableMemoryStream.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/Microsoft.Toolkit.HighPerformance.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/Microsoft.Toolkit.HighPerformance.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/PhotoShop.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/PhotoShop.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/System.Buffers.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/System.Buffers.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/System.IO.Pipelines.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/System.IO.Pipelines.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/System.Memory.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/System.Memory.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/System.Numerics.Vectors.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/System.Numerics.Vectors.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/System.Runtime.CompilerServices.Unsafe.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/System.Runtime.CompilerServices.Unsafe.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/System.Threading.Tasks.Extensions.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/System.Threading.Tasks.Extensions.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/System.ValueTuple.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/System.ValueTuple.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/TlgLib.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/TlgLib.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/Troschuetz.Random.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/Troschuetz.Random.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/VGAudio.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/VGAudio.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/XMemCompress.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/XMemCompress.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/XmpCore.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/XmpCore.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/ZstdNet.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/ZstdNet.dll -------------------------------------------------------------------------------- /Galgame/krkr/lib/emotedriver.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/krkr/lib/emotedriver.dll -------------------------------------------------------------------------------- /Galgame/luac.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Galgame/luac.exe -------------------------------------------------------------------------------- /Galgame/sprite/TextCleaner_sprite.py: -------------------------------------------------------------------------------- 1 | import json 2 | import argparse 3 | from tqdm import tqdm 4 | from glob import glob 5 | 6 | def parse_args(args=None, namespace=None): 7 | parser = argparse.ArgumentParser() 8 | parser.add_argument("-JA", type=str, default=r'E:\Games\Galgame\sprite\Ao no Kanata no Four Rhythm\Aokana_Data\system_ext\scripts') 9 | parser.add_argument("-op", type=str, default=r'D:\Fuck_galgame\index.json') 10 | return parser.parse_args(args=args, namespace=namespace) 11 | 12 | def text_cleaning(text): 13 | text = text.replace('『', '').replace('』', '').replace('「', '').replace('」', '').replace('(', '').replace(')', '').replace('“', '').replace('”', '').replace('≪', '').replace('≫', '') 14 | text = text.replace('\n', '').replace(r'\n', '').replace(r' ', '').replace('♪', '').replace('♥', '').replace('%r', '') 15 | return text 16 | 17 | def read_bs5(JA_dir, op_json): 18 | files_original = glob(f"{JA_dir}/**/*.bs5", recursive=True) 19 | result = [] 20 | seen = set() 21 | for filename in tqdm(files_original): 22 | with open(filename, 'r', encoding='utf-8') as file: 23 | lines = [line.strip() for line in file.readlines() if line.strip()] 24 | 25 | i = 0 26 | j = 0 27 | while i < len(lines): 28 | line = lines[i] 29 | if line.startswith("voice0"): 30 | elements1 = line.split('\t') 31 | if len(elements1) == 2: 32 | Voice = elements1[1].split('//')[0] 33 | if i < len(lines) and Voice not in seen: 34 | seen.add(Voice) 35 | for j in range(i + 1, len(lines)): 36 | next_line = lines[j] 37 | if (not next_line.startswith("//")) and (not next_line.startswith("@")): 38 | try: 39 | elements2 = next_line.split('␂')[2].split(':') 40 | Speaker = elements2[0].replace('【', '').replace('】', '') 41 | Text = elements2[1] 42 | Text = text_cleaning(Text) 43 | result.append((Speaker, Voice, Text)) 44 | break 45 | except: 46 | break 47 | i += 1 48 | 49 | with open(op_json, mode='w', encoding='utf-8') as file: 50 | json_data = [{'Speaker': Speaker, 'Voice': Voice, 'Text': Text} for Speaker, Voice, Text in result] 51 | json.dump(json_data, file, ensure_ascii=False, indent=4) 52 | 53 | if __name__ == '__main__': 54 | args = parse_args() 55 | read_bs5(args.JA, args.op) -------------------------------------------------------------------------------- /Galgame/sprite/decrypt.py: -------------------------------------------------------------------------------- 1 | import os 2 | import struct 3 | import argparse 4 | import numpy as np 5 | from tqdm import tqdm 6 | 7 | global mod, mod1 8 | mod = 2 ** 32 9 | mod1 = 2 ** 31 10 | 11 | def gk(k): 12 | num = (k * 7391 + 42828) % mod 13 | num2 = (num << 17 ^ num) % mod 14 | out = [] 15 | for i in range(256): 16 | num = (num - k + num2) % mod 17 | num2 = (num + 56) % mod 18 | num = (num * (num2 & 239)) % mod 19 | out.append(num & 255) 20 | num = num >> 1 21 | return out 22 | 23 | def dd(data, k): 24 | arr = np.frombuffer(data, dtype=np.uint8).copy() 25 | key = np.array(gk(k), dtype=np.uint8) 26 | n = arr.size 27 | indices_253 = np.arange(n) % 253 28 | indices_89 = np.arange(n) % 89 29 | arr = np.bitwise_xor(arr, key[indices_253]) 30 | arr = arr + 3 + key[indices_89] 31 | arr = np.bitwise_xor(arr, 153) 32 | return arr.tobytes() 33 | 34 | def getInfo(f): 35 | f.seek(0) 36 | header = f.read(1024) 37 | num = 0 38 | num = (sum(struct.unpack(251 * "i", header[16:-4])) + mod1) % mod - mod1 39 | raw = dd(f.read(16 * num), struct.unpack("I", header[212:216])[0]) 40 | start = struct.unpack("I", raw[12:16])[0] 41 | array = dd(f.read(start - 1024 - 16 * num), struct.unpack("I", header[92:96])[0]) 42 | out = [] 43 | for i in range(num): 44 | l, offset, k, p = struct.unpack("IIII", raw[16 * i:16 * (i + 1)]) 45 | name = array[offset:array.find(0, offset)].decode("ascii") 46 | out.append((name, p, l, k)) 47 | return out 48 | 49 | def extract(f, files, out): 50 | for name, p, l, k in tqdm(files): 51 | name = os.path.join(out, name) 52 | os.makedirs(os.path.dirname(name), exist_ok=True) 53 | with open(name, "wb") as o: 54 | f.seek(p) 55 | data = dd(f.read(l), k) 56 | o.write(data) 57 | 58 | if __name__ == "__main__": 59 | parser = argparse.ArgumentParser(description="Process a .dat file and optionally extract contents.") 60 | parser.add_argument("--input_path", default=r"E:\Games\Galgame\sprite\Ao no Kanata no Four Rhythm\Aokana_Data\system.dat") 61 | parser.add_argument("--output_path", default=r"E:\Games\Galgame\sprite\Ao no Kanata no Four Rhythm\Aokana_Data\system_ext") 62 | 63 | args = parser.parse_args() 64 | 65 | f = open(args.input_path, "rb") 66 | files = getInfo(f) 67 | 68 | if args.output_path: 69 | extract(f, files, args.output_path) -------------------------------------------------------------------------------- /Other/Calculater_Total_Duration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from mutagen.wave import WAVE 4 | from mutagen.oggvorbis import OggVorbis 5 | from mutagen.oggopus import OggOpus 6 | from concurrent.futures import ProcessPoolExecutor 7 | from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn, MofNCompleteColumn 8 | 9 | rich_progress = Progress(TextColumn("Running: "), BarColumn(), "[progress.percentage]{task.percentage:>3.1f}%", "•", MofNCompleteColumn(), "•", TimeElapsedColumn(), "|", TimeRemainingColumn(), refresh_per_second=5) 10 | 11 | def sec_to_time(total_seconds): 12 | hours = total_seconds // 3600 13 | minutes = (total_seconds % 3600) // 60 14 | seconds = total_seconds % 60 15 | return int(hours), int(minutes), seconds 16 | 17 | def calculate_durations(filenames): 18 | total_duration_sec = 0 19 | max_duration_sec = 0 20 | min_duration_sec = float('inf') 21 | 22 | with rich_progress: 23 | task2 = rich_progress.add_task("Processing", total=len(filenames)) 24 | 25 | for filename in filenames: 26 | try: 27 | if filename.endswith('.wav'): 28 | audio = WAVE(filename) 29 | elif filename.endswith('.ogg'): 30 | audio = OggVorbis(filename) 31 | elif filename.endswith('.opus'): 32 | audio = OggOpus(filename) 33 | else: 34 | raise ValueError(f"Unsupported file format: {filename}") 35 | 36 | file_duration_sec = audio.info.length # Duration in seconds 37 | total_duration_sec += file_duration_sec 38 | max_duration_sec = max(max_duration_sec, file_duration_sec) 39 | min_duration_sec = min(min_duration_sec, file_duration_sec) 40 | rich_progress.update(task2, advance=1) 41 | 42 | except Exception as e: 43 | print(f"Error processing {filename}: {e}") 44 | 45 | return total_duration_sec, max_duration_sec, min_duration_sec 46 | 47 | def parallel_process(filenames, num_processes): 48 | results = [] 49 | with ProcessPoolExecutor(max_workers=num_processes) as executor: 50 | tasks = [executor.submit(calculate_durations, filenames[int(i * len(filenames) / num_processes): int((i + 1) * len(filenames) / num_processes)]) for i in range(num_processes)] 51 | for future in tasks: 52 | results.append(future.result()) 53 | return results 54 | 55 | def aggregate_results(results): 56 | total_duration_sec = sum(result[0] for result in results) 57 | max_duration_sec = max(result[1] for result in results) 58 | min_duration_sec = min(result[2] for result in results if result[2] != float('inf')) # Avoiding inf if possible 59 | return sec_to_time(total_duration_sec), sec_to_time(max_duration_sec), sec_to_time(min_duration_sec) 60 | 61 | def main(in_dir, num_processes): 62 | print('Loading audio files...') 63 | extensions = ["wav", "mp3", "ogg", "flac", "opus", "snd"] 64 | filenames = [] 65 | 66 | with rich_progress: 67 | task1 = rich_progress.add_task("Loading", total=None) 68 | for root, _, files in os.walk(in_dir): 69 | for file in files: 70 | if file.lower().endswith(tuple(extensions)): 71 | filenames.append(os.path.join(root, file)) 72 | rich_progress.update(task1, advance=1) 73 | rich_progress.update(task1, total=len(filenames), completed=len(filenames)) 74 | 75 | print("==========================================================================") 76 | 77 | if filenames: 78 | results = parallel_process(filenames, num_processes) 79 | total_duration, max_duration, min_duration = aggregate_results(results) 80 | print("==========================================================================") 81 | print(f"SUM: {len(filenames)} files\n") 82 | print(f"SUM: {total_duration[0]:02d}:{total_duration[1]:02d}:{total_duration[2]:05.2f}") 83 | print(f"MAX: {max_duration[0]:02d}:{max_duration[1]:02d}:{max_duration[2]:05.2f}") 84 | print(f"MIN: {min_duration[0]:02d}:{min_duration[1]:02d}:{min_duration[2]:05.2f}") 85 | return(f'{total_duration[0]:02d}:{total_duration[1]:02d}:{total_duration[2]:05.2f}') 86 | 87 | else: 88 | print("No audio files found.") 89 | 90 | if __name__ == "__main__": 91 | parser = argparse.ArgumentParser() 92 | parser.add_argument("--in_dir", type=str, default=r"D:\1") 93 | parser.add_argument('--num_processes', type=int, default=18) 94 | args = parser.parse_args() 95 | 96 | main(args.in_dir, args.num_processes) -------------------------------------------------------------------------------- /Other/Calculater_Total_Duration_F.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from concurrent.futures import ThreadPoolExecutor, as_completed 4 | from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn, MofNCompleteColumn 5 | import av 6 | 7 | AUDIO_EXTENSIONS = (".wav", ".mp3", ".ogg", ".flac", ".opus") 8 | 9 | def process_audio_file(file_path): 10 | try: 11 | with av.open(file_path, metadata_errors="ignore") as container: 12 | duration = container.duration / 1_000_000 13 | return duration 14 | except Exception as e: 15 | print(f"Error processing {file_path}: {e}") 16 | return 0 17 | 18 | def main(in_dir, num_threads): 19 | audio_files = [] 20 | total_duration = 0 21 | 22 | for root, _, files in os.walk(in_dir): 23 | for file in files: 24 | if file.lower().endswith(AUDIO_EXTENSIONS): 25 | full_path = os.path.join(root, file) 26 | audio_files.append(full_path) 27 | 28 | if not audio_files: 29 | print("未在指定目录中找到音频文件。") 30 | return 31 | 32 | with Progress(TextColumn("[progress.description]{task.description}"), BarColumn(bar_width=100), "[progress.percentage]{task.percentage:>3.2f}%", "•", MofNCompleteColumn(), "•", TimeElapsedColumn(), "|", TimeRemainingColumn()) as progress: 33 | total_task = progress.add_task("Total", total=len(audio_files)) 34 | with ThreadPoolExecutor(max_workers=num_threads) as executor: 35 | futures = {executor.submit(process_audio_file, file): file for file in audio_files} 36 | for future in as_completed(futures): 37 | duration = future.result() 38 | total_duration += duration 39 | progress.update(total_task, advance=1) 40 | 41 | print(f"Total duration of all audio files: {total_duration:.2f} seconds") 42 | 43 | if __name__ == "__main__": 44 | parser = argparse.ArgumentParser() 45 | parser.add_argument("--in_dir", type=str, default=r"D:\\Voice") 46 | parser.add_argument("--num_threads", type=int, default=20) 47 | args = parser.parse_args() 48 | main(args.in_dir, args.num_threads) -------------------------------------------------------------------------------- /Other/Combiner_Batch.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import wave 4 | from pydub import AudioSegment 5 | from tqdm import tqdm 6 | 7 | input_dir = r"D:\vcdata\svcdata" 8 | output_dir = r"D:\vcdata\1" 9 | 10 | wav_files_list = [] 11 | 12 | 13 | def concatenate_wavs(wav_list, output_path, subdir): 14 | with wave.open(output_path, 'wb') as output: 15 | for wav_file in wav_list: 16 | with wave.open(wav_file, 'rb') as w: 17 | if not output.getnframes(): 18 | output.setnchannels(1) # 设置单声道 19 | output.setsampwidth(2) # 设置采样宽度为16bit 20 | output.setframerate(44100) # 设置采样率为44.1kHz 21 | output.writeframes(w.readframes(w.getnframes())) 22 | 23 | for subdir in tqdm(os.listdir(input_dir), desc=f"Processing"): 24 | subdir_path = os.path.join(input_dir, subdir) 25 | if os.path.isdir(subdir_path): 26 | wav_files = glob.glob(os.path.join(subdir_path, '*.wav')) 27 | wav_files_list.extend(wav_files) 28 | wav_files_list = sorted(wav_files_list) 29 | 30 | for i in range(0, len(wav_files_list), 500): 31 | sub_list = wav_files_list[i:i+500] 32 | output_wav = os.path.join(output_dir, f'merge_{subdir}_{i//500}.wav') 33 | concatenate_wavs(sub_list, output_wav, subdir) 34 | sub_list = [] 35 | wav_files_list = [] 36 | -------------------------------------------------------------------------------- /Other/Combiner_To_6s.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import wave 4 | import shutil 5 | from tqdm import tqdm 6 | 7 | input_dir = r"/home/ooppeenn/Desktop/2" 8 | output_dir = r"/home/ooppeenn/Desktop/3" 9 | 10 | unclassified_wav_list = [] 11 | short_wav_list = [] 12 | total_duration = 0 13 | start_index, end_index = 0, 0 14 | 15 | def concatenate_wavs(wav_list, output_path): 16 | first_wav = wave.open(wav_list[0], 'rb') 17 | output_wav = wave.open(output_path, 'wb') 18 | output_wav.setnchannels(first_wav.getnchannels()) 19 | output_wav.setsampwidth(first_wav.getsampwidth()) 20 | output_wav.setframerate(first_wav.getframerate()) 21 | output_wav.setnframes(0) 22 | for wav_file in wav_list: 23 | input_wav = wave.open(wav_file, 'rb') 24 | output_wav.writeframes(input_wav.readframes(input_wav.getnframes())) 25 | input_wav.close() 26 | output_wav.close() 27 | first_wav.close() 28 | 29 | def get_audio_duration(file_path): 30 | with wave.open(file_path, 'r') as audio_file: 31 | frames = audio_file.getnframes() 32 | rate = audio_file.getframerate() 33 | duration = frames / float(rate) 34 | return duration 35 | 36 | for subdir in tqdm(os.listdir(input_dir)): 37 | subdir_path = os.path.join(input_dir, subdir) 38 | if os.path.isdir(subdir_path): 39 | wav_files = glob.glob(os.path.join(subdir_path, '*.wav')) 40 | unclassified_wav_list.extend(wav_files) 41 | unclassified_wav_list = sorted(unclassified_wav_list) 42 | 43 | for unclassified_wav in unclassified_wav_list: 44 | if get_audio_duration(unclassified_wav) >= 6: 45 | output_subdir = os.path.join(output_dir, subdir) 46 | os.makedirs(output_subdir, exist_ok=True) 47 | shutil.copy(unclassified_wav, output_subdir) 48 | else: 49 | short_wav_list.append(unclassified_wav) 50 | 51 | short_wav_list = sorted(short_wav_list) 52 | 53 | for short_wav_file in short_wav_list: 54 | single_duration = get_audio_duration(short_wav_file) 55 | total_duration += single_duration 56 | if total_duration < 6: 57 | end_index += 1 58 | else: 59 | output_subdir = os.path.join(output_dir, subdir) 60 | os.makedirs(output_subdir, exist_ok=True) 61 | output_wav = os.path.join(output_subdir, f'merge_{subdir}_{start_index}_{end_index}.wav') 62 | concatenate_wavs(short_wav_list[start_index:end_index + 1], output_wav) 63 | start_index = end_index + 1 64 | end_index = start_index 65 | total_duration = 0 66 | total_duration = 0 67 | start_index, end_index = 0, 0 68 | short_wav_list = [] 69 | unclassified_wav_list = [] -------------------------------------------------------------------------------- /Other/Cutter.py: -------------------------------------------------------------------------------- 1 | import os 2 | from glob import glob 3 | from tqdm import tqdm 4 | from concurrent.futures import ProcessPoolExecutor 5 | from pydub import AudioSegment, silence 6 | 7 | def main(input_base_path, output_base_path, files, silence_duration_ms): 8 | for file_path in tqdm(files): 9 | audio = AudioSegment.from_file(file_path) 10 | chunks = silence.split_on_silence(audio, min_silence_len=100, silence_thresh=-40, keep_silence=300) 11 | 12 | accumulated_chunk = AudioSegment.empty() 13 | i = 0 14 | for chunk in chunks: 15 | if len(chunk) < 5000: 16 | if not accumulated_chunk: 17 | accumulated_chunk = chunk 18 | else: 19 | accumulated_chunk += AudioSegment.silent(duration=silence_duration_ms) + chunk 20 | 21 | if len(accumulated_chunk) >= 5000 or i == len(chunks) - 1: 22 | chunk_to_process = accumulated_chunk 23 | accumulated_chunk = AudioSegment.empty() 24 | else: 25 | continue 26 | else: 27 | chunk_to_process = chunk 28 | 29 | relative_path = os.path.relpath(file_path, input_base_path) 30 | output_dir = os.path.join(output_base_path, os.path.dirname(relative_path)) 31 | os.makedirs(output_dir, exist_ok=True) 32 | filename = os.path.splitext(os.path.basename(file_path))[0] 33 | output_path = os.path.join(output_dir, f"{filename}_chunk{i}.wav") 34 | chunk_to_process.export(output_path, format="wav") 35 | i += 1 36 | 37 | if __name__ == "__main__": 38 | input_folder_path = r"E:\Dataset\東北きりたん" 39 | output_folder_path = r"E:\Dataset\東北きりたん_cut" 40 | num_processes = 20 41 | silence_duration_ms = 100 # 拼接之间的静音时长,单位毫秒 42 | 43 | extensions = ['wav', 'ogg', 'opus', 'snd'] 44 | files = [] 45 | for ext in extensions: 46 | files.extend(glob(f"{input_folder_path}/**/*.{ext}", recursive=True)) 47 | 48 | with ProcessPoolExecutor(max_workers=num_processes) as executor: 49 | tasks = [executor.submit(main, input_folder_path, output_folder_path, files[rank::num_processes], silence_duration_ms) for rank in range(num_processes)] 50 | for task in tasks: 51 | task.result() -------------------------------------------------------------------------------- /Other/Deleter_Blank.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from glob import glob 4 | from tqdm import tqdm 5 | import multiprocessing 6 | from concurrent.futures import ProcessPoolExecutor 7 | from pydub import AudioSegment 8 | from pydub.silence import split_on_silence 9 | 10 | def cut_silence(input_file, output_file, silence_threshold=-60, min_silence_len=5000): 11 | audio = AudioSegment.from_file(input_file, format="wav") 12 | chunks = split_on_silence(audio, silence_thresh=silence_threshold, min_silence_len=min_silence_len, keep_silence=0) 13 | result = AudioSegment.empty() 14 | for chunk in chunks: 15 | result += chunk 16 | result.export(output_file, format="wav") 17 | 18 | def process_one(file, input_folder, output_folder, silence_threshold=-60, min_silence_len=5000): 19 | input_file = file 20 | output_file = os.path.join(output_folder, os.path.relpath(input_file, start=input_folder)) 21 | output_file_dir = os.path.dirname(output_file) 22 | os.makedirs(output_file_dir, exist_ok=True) 23 | audio = AudioSegment.from_file(input_file, format="wav") 24 | chunks = split_on_silence(audio, silence_thresh=silence_threshold, min_silence_len=min_silence_len, keep_silence=0) 25 | result = AudioSegment.empty() 26 | for chunk in chunks: 27 | result += chunk 28 | result.export(output_file, format="wav") 29 | 30 | def process_batch(file_chunk, input_folder, output_folder): 31 | for file in tqdm(file_chunk): 32 | process_one(file, input_folder, output_folder) 33 | 34 | def parallel_process(files, input_folder, output_folder, num_processes): 35 | with ProcessPoolExecutor(max_workers=num_processes) as executor: 36 | tasks = [] 37 | for i in range(num_processes): 38 | start = int(i * len(files) / num_processes) 39 | end = int((i + 1) * len(files) / num_processes) 40 | file_chunk = files[start:end] 41 | tasks.append(executor.submit(process_batch, file_chunk, input_folder, output_folder)) 42 | for task in tqdm(tasks): 43 | task.result() 44 | 45 | if __name__ == "__main__": 46 | parser = argparse.ArgumentParser() 47 | parser.add_argument("--input_folder", type=str, default=r"E:\Dataset\東北イタコ_de", help="path to input dir") 48 | parser.add_argument('--output_folder', type=str, default=r"E:\Dataset\東北イタコ_clean", help='path to output dir') 49 | parser.add_argument('--num_processes', type=int, default=10, help='set the number of processes') 50 | args = parser.parse_args() 51 | output_folder = args.output_folder 52 | input_folder = args.input_folder 53 | 54 | files = glob(f"{input_folder}/**.wav", recursive=True) 55 | 56 | multiprocessing.set_start_method("spawn", force=True) 57 | num_processes = args.num_processes 58 | if num_processes == 0: 59 | num_processes = os.cpu_count() 60 | 61 | parallel_process(files, input_folder, output_folder, num_processes) -------------------------------------------------------------------------------- /Other/Deleter_Over_30s.py: -------------------------------------------------------------------------------- 1 | import os 2 | import wave 3 | from tqdm import tqdm 4 | 5 | def get_wav_duration(wav_file): 6 | try: 7 | with wave.open(wav_file, 'rb') as wf: 8 | frames = wf.getnframes() 9 | rate = wf.getframerate() 10 | duration = frames / float(rate) 11 | return duration 12 | except Exception as e: 13 | print(f"Error reading {wav_file}: {e}") 14 | return None 15 | 16 | def delete_short_wav_files(folder_path, tag_duration=0.5): 17 | for root, _, files in tqdm(os.walk(folder_path)): 18 | for file in files: 19 | if file.endswith('.wav'): 20 | wav_file = os.path.join(root, file) 21 | duration = get_wav_duration(wav_file) 22 | if duration is not None and duration < tag_duration: 23 | print(f"Deleting {wav_file} (Duration: {duration} seconds)") 24 | os.remove(wav_file) 25 | 26 | if __name__ == "__main__": 27 | folder_path = r'C:\Users\bfloat16\Desktop\1212112112' 28 | delete_short_wav_files(folder_path) 29 | -------------------------------------------------------------------------------- /Other/Resampler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import av 3 | import argparse 4 | import av.audio 5 | import av.audio.resampler 6 | import numpy as np 7 | import pyloudnorm as pyln 8 | import torch.multiprocessing as mp 9 | from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn, MofNCompleteColumn 10 | 11 | rich_progress = Progress(TextColumn("Running: "), BarColumn(), "[progress.percentage]{task.percentage:>3.1f}%", "•", MofNCompleteColumn(), "•", TimeElapsedColumn(), "|", TimeRemainingColumn(), refresh_per_second=5) 12 | 13 | def save_as_mp3(audio, filename, in_dir, out_dir, sample_rate): 14 | rel_path = os.path.relpath(filename, in_dir) 15 | output_path = os.path.join(out_dir, os.path.splitext(rel_path)[0] + ".mp3") 16 | 17 | os.makedirs(os.path.dirname(output_path), exist_ok=True) 18 | 19 | container = av.open(output_path, mode='w') 20 | 21 | stream = container.add_stream('mp3', rate=sample_rate) 22 | stream.bit_rate = 320000 23 | stream.format = 's16p' 24 | stream.layout = 'mono' 25 | 26 | frame = av.AudioFrame.from_ndarray(audio, format='flt', layout='mono') 27 | frame.sample_rate = sample_rate 28 | 29 | for packet in stream.encode(frame): 30 | container.mux(packet) 31 | 32 | for packet in stream.encode(): 33 | container.mux(packet) 34 | 35 | container.close() 36 | 37 | def process_batch(rank, filelist, in_dir, out_dir, target_sr, num_process): 38 | filenames = filelist[rank::num_process] 39 | print(f"\nProcess {rank} - Number of files: {len(filenames)}") 40 | 41 | with Progress() as rich_progress: 42 | task2 = rich_progress.add_task("Processing", total=len(filenames)) 43 | 44 | for filename in filenames: 45 | try: 46 | input_container = av.open(filename) 47 | input_stream = input_container.streams.audio[0] 48 | resampler = av.AudioResampler(format='s16', layout='mono', rate=target_sr) 49 | 50 | audio_frames = [] 51 | for frame in input_container.decode(input_stream): 52 | if isinstance(frame, av.AudioFrame): 53 | resampled_frame = resampler.resample(frame) 54 | audio_frames.append(resampled_frame) 55 | 56 | if not audio_frames: 57 | raise ValueError(f"No valid audio frames in {filename}") 58 | 59 | audio = np.hstack([frame.to_ndarray().flatten() for frame in audio_frames]) 60 | duration = len(audio) / target_sr 61 | 62 | if duration > 30 or duration < 1: 63 | print(f"Skip: {filename} - Duration: {duration:.2f}s") 64 | continue 65 | 66 | peak_normalized_audio = pyln.normalize.peak(audio, -1.0) 67 | 68 | meter = pyln.Meter(target_sr) 69 | loudness = meter.integrated_loudness(peak_normalized_audio) 70 | loudness_normalized_audio = pyln.normalize.loudness(peak_normalized_audio, loudness, -23.0) 71 | 72 | audio_f = np.expand_dims(loudness_normalized_audio, axis=0) 73 | save_as_mp3(audio_f, filename, in_dir, out_dir, target_sr) 74 | 75 | except Exception as e: 76 | print(f"Error: {filename}: {e}") 77 | rich_progress.update(task2, advance=1) 78 | 79 | def main(in_dir): 80 | print('Loading audio files...') 81 | extensions = ["wav", "mp3", "ogg", "flac", "opus", "snd"] 82 | filenames = [] 83 | 84 | with rich_progress: 85 | task1 = rich_progress.add_task("Loading", total=None) 86 | for root, _, files in os.walk(in_dir): 87 | for file in files: 88 | if file.lower().endswith(tuple(extensions)): 89 | filenames.append(os.path.join(root, file)) 90 | rich_progress.update(task1, advance=1) 91 | rich_progress.update(task1, total=len(filenames), completed=len(filenames)) 92 | 93 | print("==========================================================================") 94 | return filenames 95 | 96 | if __name__ == "__main__": 97 | parser = argparse.ArgumentParser() 98 | parser.add_argument("--in_dir", type=str, default=r"D:\Dataset\EN-B000000") 99 | parser.add_argument("--out_dir", type=str, default=r"D:\Dataset_16k") 100 | parser.add_argument("--target_sr", type=int, default=16000) 101 | parser.add_argument('--num_process', type=int, default=1) 102 | args = parser.parse_args() 103 | 104 | os.makedirs(args.out_dir, exist_ok=True) 105 | 106 | filelist = main(args.in_dir) 107 | print(f'Number of files: {len(filelist)}') 108 | print('Start Resample...') 109 | 110 | mp.spawn(process_batch, args=(filelist, args.in_dir, args.out_dir, args.target_sr, args.num_process), nprocs=args.num_process, join=True) -------------------------------------------------------------------------------- /Other/SAMI.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import requests 4 | import sys 5 | import threading 6 | 7 | from volcengine.ApiInfo import ApiInfo 8 | from volcengine.Credentials import Credentials 9 | from volcengine.ServiceInfo import ServiceInfo 10 | from volcengine.base.Service import Service 11 | 12 | ACCESS_KEY = '' 13 | SECRET_KEY = '' 14 | APPKEY = '' 15 | AUTH_VERSION = 'volc-auth-v1' 16 | 17 | class SAMIService(Service): 18 | _instance_lock = threading.Lock() 19 | 20 | def __new__(cls, *args, **kwargs): 21 | if not hasattr(SAMIService, "_instance"): 22 | with SAMIService._instance_lock: 23 | if not hasattr(SAMIService, "_instance"): 24 | SAMIService._instance = object.__new__(cls) 25 | return SAMIService._instance 26 | 27 | def __init__(self): 28 | self.service_info = SAMIService.get_service_info() 29 | self.api_info = SAMIService.get_api_info() 30 | super(SAMIService, self).__init__(self.service_info, self.api_info) 31 | 32 | @staticmethod 33 | def get_service_info(): 34 | api_url = 'open.volcengineapi.com' 35 | service_info = ServiceInfo(api_url, {}, Credentials('', '', 'sami', 'cn-north-1'), 10, 10) 36 | return service_info 37 | 38 | @staticmethod 39 | def get_api_info(): 40 | api_info = {"GetToken": ApiInfo("POST", "/", {"Action": "GetToken", "Version": "2021-07-27"}, {}, {}),} 41 | return api_info 42 | 43 | def common_json_handler(self, api, body): 44 | params = dict() 45 | try: 46 | body = json.dumps(body) 47 | res = self.json(api, params, body) 48 | res_json = json.loads(res) 49 | return res_json 50 | except Exception as e: 51 | res = str(e) 52 | try: 53 | res_json = json.loads(res) 54 | return res_json 55 | except: 56 | raise Exception(str(e)) 57 | 58 | 59 | if __name__ == '__main__': 60 | sami_service = SAMIService() 61 | sami_service.set_ak(ACCESS_KEY) 62 | sami_service.set_sk(SECRET_KEY) 63 | 64 | req = {"appkey": APPKEY, "token_version": AUTH_VERSION, "expiration": 3600} 65 | resp = sami_service.common_json_handler("GetToken", req) 66 | try: 67 | token = resp["token"] 68 | print("response task_id=%s status_code=%d status_text=%s expires_at=%s\n\t token=%s" %(resp["task_id"], resp["status_code"], resp["status_text"],resp["expires_at"], token)) 69 | except: 70 | print("get token failed, ", resp) 71 | sys.exit(1) 72 | 73 | model = "bs_4track_vocal" 74 | payload = json.dumps({"model": model}) 75 | with open(r"C:\Users\bfloat16\Desktop\Music\only my railgun.flac", "rb") as f: 76 | data = f.read() 77 | data = base64.b64encode(data).decode('utf-8') 78 | req = {"appkey": "nMYMaXjBNo", "token": token, "namespace": "MusicSourceSeparate", "payload": payload, "data": data} 79 | resp = requests.post("https://sami.bytedance.com/api/v1/invoke", json=req) 80 | try: 81 | sami_resp = resp.json() 82 | if resp.status_code != 200: 83 | print(sami_resp) 84 | sys.exit(1) 85 | except: 86 | print(resp) 87 | sys.exit(1) 88 | 89 | print("response task_id=%s status_code=%d status_text=%s" %(sami_resp["task_id"], sami_resp["status_code"], sami_resp["status_text"]), end=" ") 90 | 91 | if "payload" in sami_resp and len(sami_resp["payload"]) > 0: 92 | print("payload=%s" % sami_resp["payload"], end=" ") 93 | 94 | if "data" in sami_resp and len(sami_resp["data"]) > 0: 95 | data = base64.b64decode(sami_resp["data"]) 96 | print("data=[%d]bytes" % len(data)) 97 | with open("output.wav", "wb") as f: 98 | f.write(data) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 屎山,慢慢重构 -------------------------------------------------------------------------------- /Unity/BanG Dream!/downloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import requests 4 | import argparse 5 | from Crypto.Cipher import AES 6 | from concurrent import futures 7 | from datetime import datetime, timezone 8 | from tools import application_info_pb2, assetbundle_info_pb2 9 | from google.protobuf.json_format import MessageToDict 10 | from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn 11 | 12 | def args_parser(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("--output_dir", default=r"E:\Game_Dataset\jp.co.craftegg.band\RAW") 15 | parser.add_argument("--threads", default=32) 16 | return parser.parse_args() 17 | 18 | APP_HASH = "e15bca7f8e1c11ad1284a3ac1f9863b3a513c921c6f8a57a8fbd153c7539055c" 19 | APP_URL = "https://api.garupa.jp/api/application" 20 | ASSET_URL = "https://content.garupa.jp/Release" 21 | 22 | PROXIES= { 23 | "http": "http://127.0.0.1:7897", 24 | "https": "http://127.0.0.1:7897", 25 | } 26 | 27 | USER_AGENT = "UnityPlayer/2021.3.39f1 (UnityWebRequest/1.0, libcurl/8.5.0-DEV)" 28 | 29 | HEADERS1 = { 30 | "Host": "api.garupa.jp", 31 | "User-Agent": USER_AGENT, 32 | "Accept-Encoding": "deflate, gzip", 33 | "Content-Type": "application/octet-stream", 34 | "Accept": "application/octet-stream", 35 | "X-ClientVersion": "9.1.0", 36 | "X-Signature": "3cde36c1-b431-4458-90cf-469cb0096e0a", 37 | "X-ClientPlatform": "Android", 38 | } 39 | 40 | HEADERS2 = { 41 | "Host": "content.garupa.jp", 42 | "User-Agent": USER_AGENT, 43 | "Accept-Encoding": "deflate, gzip", 44 | "X-ClientPlatform": "Android", 45 | "X-Unity-Version": "2021.3.39f1", 46 | } 47 | 48 | columns = (SpinnerColumn(), TextColumn("[bold blue]{task.description}"), BarColumn(bar_width=100), "[progress.percentage]{task.percentage:>6.2f}%", TextColumn("{task.completed}/{task.total}"), TimeElapsedColumn(), "•", TimeRemainingColumn()) 49 | 50 | def get_asset_url_dec(data): 51 | key = b"mikumikulukaluka" 52 | iv = b"lukalukamikumiku" 53 | cipher = AES.new(key, AES.MODE_CBC, iv) 54 | padded = cipher.decrypt(data) 55 | pad_len = padded[-1] 56 | return padded[:-pad_len] 57 | 58 | def get_asset_url(): 59 | resp = requests.get(APP_URL, headers=HEADERS1, proxies=PROXIES) 60 | resp.raise_for_status() 61 | app_info = application_info_pb2.AppGetResponse() 62 | resp = get_asset_url_dec(resp.content) 63 | app_info.ParseFromString(resp) 64 | app_version = app_info.dataVersion 65 | 66 | timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") 67 | url_meta = f"{ASSET_URL}/{app_version}_{APP_HASH}/Android/AssetBundleInfo?t={timestamp}" 68 | url_ab = f"{ASSET_URL}/{app_version}_{APP_HASH}/Android" 69 | resp = requests.get(url_meta, headers=HEADERS2, proxies=PROXIES) 70 | resp.raise_for_status() 71 | 72 | info = assetbundle_info_pb2.AssetBundleInfo() 73 | info.ParseFromString(resp.content) 74 | bundle = MessageToDict(info) 75 | return bundle, url_ab 76 | 77 | def handler_assetbundle_info(root, data): 78 | bundles = [] 79 | total_filesize = 0 80 | filters = ["scenario", "story", "sound"] 81 | 82 | with Progress(*columns, transient=True) as progress: 83 | task_id = progress.add_task("Checking", total=None) 84 | for item in data["Bundles"].values(): 85 | name = item["BundleName"] + ".unity3d" 86 | #if not any(name.startswith(f) for f in filters): 87 | #continue 88 | total_filesize += int(item["FileSize"]) 89 | bundles.append(name) 90 | 91 | progress.update(task_id, advance=1) 92 | total_filesize = total_filesize / (1024 ** 3) 93 | return bundles, total_filesize 94 | 95 | def worker(bundle_name, base_url, out_root): 96 | dest_path = os.path.join(out_root, bundle_name) 97 | os.makedirs(os.path.dirname(dest_path), exist_ok=True) 98 | 99 | session = requests.Session() 100 | session.proxies.update(PROXIES) 101 | session.headers.update({"User-Agent": USER_AGENT}) 102 | 103 | while True: 104 | timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S") 105 | url = f"{base_url}/{bundle_name.replace(".unity3d", "")}?t={timestamp}" 106 | try: 107 | resp = session.get(url) 108 | resp.raise_for_status() 109 | with open(dest_path, 'wb') as f: 110 | f.write(resp.content) 111 | return 112 | except Exception as exc: 113 | print(f"[Warning] {bundle_name}") 114 | time.sleep(1) 115 | 116 | if __name__ == "__main__": 117 | args = args_parser() 118 | bundle_info_dict, url_ab = get_asset_url() 119 | bundles, total_filesize = handler_assetbundle_info(args.output_dir, bundle_info_dict) 120 | 121 | print(f"Total payload: {total_filesize:.2f} GiB ({len(bundles):,} files)") 122 | 123 | os.makedirs(args.output_dir, exist_ok=True) 124 | 125 | with Progress(*columns) as progress: 126 | task_id = progress.add_task("Downloading", total=len(bundles)) 127 | with futures.ThreadPoolExecutor(max_workers=args.threads) as executor: 128 | future_list = [executor.submit(worker, name, url_ab, args.output_dir) for name in bundles] 129 | for _ in futures.as_completed(future_list): 130 | progress.update(task_id, advance=1) -------------------------------------------------------------------------------- /Unity/BanG Dream!/script.py: -------------------------------------------------------------------------------- 1 | import json 2 | import argparse 3 | from glob import glob 4 | 5 | def args_parser(): 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument("--root", default=r"E:\Game_Dataset\jp.co.craftegg.band\EXP\Story") 8 | parser.add_argument("--output", default=r"E:\Game_Dataset\jp.co.craftegg.band\EXP\index.json") 9 | return parser.parse_args() 10 | 11 | def clean_text(text): 12 | text = text.replace("「", "").replace("」", "").replace("『", "").replace("』", "").replace("【", "").replace("】", "") 13 | text = text.replace("\\n", "\n").replace("\n", "").replace('\\"', '"') 14 | text = text.replace("\u3000", "") 15 | return text 16 | 17 | def main(): 18 | args = args_parser() 19 | root = args.root 20 | output = args.output 21 | 22 | filelist = glob(f"{root}/**/*.json", recursive=True) 23 | result = [] 24 | for file in filelist: 25 | with open(file, "r", encoding="utf-8") as f: 26 | data = json.load(f) 27 | major = data.get("scenarioSceneId") 28 | if major is None: 29 | continue 30 | talkData = data["talkData"] 31 | for i in range(len(talkData)): 32 | Speaker = talkData[i]["windowDisplayName"] 33 | Text = talkData[i]["body"] 34 | Text = clean_text(Text) 35 | Voice = talkData[i]["voices"] 36 | if len(Voice) == 0: 37 | continue 38 | else: 39 | Voice = Voice[0]["voiceId"] 40 | result.append({"major": major, "minori": i, "Speaker": Speaker, "Voice": Voice, "Text": Text}) 41 | 42 | with open(output, "w", encoding="utf-8") as f: 43 | json.dump(result, f, ensure_ascii=False, indent=4) 44 | 45 | print(len(result)) 46 | 47 | if __name__ == "__main__": 48 | main() -------------------------------------------------------------------------------- /Unity/BanG Dream!/tools/application_info.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package application_info; 4 | 5 | message AppGetResponse { 6 | reserved 6; 7 | 8 | string clientVersion = 1; 9 | string dataVersion = 2; 10 | string appStatus = 3; 11 | string clientStatus = 4; 12 | string schema = 5; 13 | string gacha = 7; 14 | string multiLive = 8; 15 | string starShop = 9; 16 | string masterDataVersion = 10; 17 | string photonAppId = 11; 18 | } -------------------------------------------------------------------------------- /Unity/BanG Dream!/tools/application_info_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: application_info.proto 4 | # Protobuf Python Version: 4.25.7 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x61pplication_info.proto\x12\x10\x61pplication_info\"\xdf\x01\n\x0e\x41ppGetResponse\x12\x15\n\rclientVersion\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x61taVersion\x18\x02 \x01(\t\x12\x11\n\tappStatus\x18\x03 \x01(\t\x12\x14\n\x0c\x63lientStatus\x18\x04 \x01(\t\x12\x0e\n\x06schema\x18\x05 \x01(\t\x12\r\n\x05gacha\x18\x07 \x01(\t\x12\x11\n\tmultiLive\x18\x08 \x01(\t\x12\x10\n\x08starShop\x18\t \x01(\t\x12\x19\n\x11masterDataVersion\x18\n \x01(\t\x12\x13\n\x0bphotonAppId\x18\x0b \x01(\tJ\x04\x08\x06\x10\x07\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'application_info_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_APPGETRESPONSE']._serialized_start=45 25 | _globals['_APPGETRESPONSE']._serialized_end=268 26 | # @@protoc_insertion_point(module_scope) 27 | -------------------------------------------------------------------------------- /Unity/BanG Dream!/tools/assetbundle_info.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package assetbundle_info; 4 | 5 | message AssetBundleElement { 6 | string BundleName = 1; 7 | string Hash = 2; 8 | string Version = 3; 9 | string Category = 4; 10 | uint32 Crc = 5; 11 | repeated string Dependencies = 6; 12 | int64 FileSize = 7; 13 | } 14 | 15 | message AssetBundleInfo { 16 | string Version = 1; 17 | map Bundles = 2; 18 | } 19 | -------------------------------------------------------------------------------- /Unity/BanG Dream!/tools/assetbundle_info_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: assetbundle_info.proto 4 | # Protobuf Python Version: 4.25.7 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x61ssetbundle_info.proto\x12\x10\x61ssetbundle_info\"\x8e\x01\n\x12\x41ssetBundleElement\x12\x12\n\nBundleName\x18\x01 \x01(\t\x12\x0c\n\x04Hash\x18\x02 \x01(\t\x12\x0f\n\x07Version\x18\x03 \x01(\t\x12\x10\n\x08\x43\x61tegory\x18\x04 \x01(\t\x12\x0b\n\x03\x43rc\x18\x05 \x01(\r\x12\x14\n\x0c\x44\x65pendencies\x18\x06 \x03(\t\x12\x10\n\x08\x46ileSize\x18\x07 \x01(\x03\"\xb9\x01\n\x0f\x41ssetBundleInfo\x12\x0f\n\x07Version\x18\x01 \x01(\t\x12?\n\x07\x42undles\x18\x02 \x03(\x0b\x32..assetbundle_info.AssetBundleInfo.BundlesEntry\x1aT\n\x0c\x42undlesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.assetbundle_info.AssetBundleElement:\x02\x38\x01\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'assetbundle_info_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_ASSETBUNDLEINFO_BUNDLESENTRY']._options = None 25 | _globals['_ASSETBUNDLEINFO_BUNDLESENTRY']._serialized_options = b'8\001' 26 | _globals['_ASSETBUNDLEELEMENT']._serialized_start=45 27 | _globals['_ASSETBUNDLEELEMENT']._serialized_end=187 28 | _globals['_ASSETBUNDLEINFO']._serialized_start=190 29 | _globals['_ASSETBUNDLEINFO']._serialized_end=375 30 | _globals['_ASSETBUNDLEINFO_BUNDLESENTRY']._serialized_start=291 31 | _globals['_ASSETBUNDLEINFO_BUNDLESENTRY']._serialized_end=375 32 | # @@protoc_insertion_point(module_scope) 33 | -------------------------------------------------------------------------------- /Unity/BanG Dream!/unpacker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import argparse 4 | from tqdm import tqdm 5 | 6 | import UnityPy 7 | from UnityPy.enums.ClassIDType import ClassIDType 8 | from UnityPy.environment import Environment 9 | from UnityPy.classes import MonoBehaviour 10 | 11 | MONOBEHAVIOUR_TYPETREES = {} 12 | 13 | UnityPy.config.FALLBACK_UNITY_VERSION = "2021.3.39f1" 14 | 15 | def exportTextAsset(obj, fp, extension=".bytes"): 16 | with open(f"{fp}", "wb") as f: 17 | f.write(obj.m_Script.encode("utf-8", "surrogateescape")) 18 | return [(obj.assets_file, obj.object_reader.path_id)] 19 | 20 | def exportMonoBehaviour(obj, fp, extension= ""): 21 | export = None 22 | if obj.object_reader.serialized_type.node: 23 | export = obj.object_reader.read_typetree() 24 | elif isinstance(obj, MonoBehaviour): 25 | script_ptr = obj.m_Script 26 | if script_ptr: 27 | script = script_ptr.read() 28 | nodes = MONOBEHAVIOUR_TYPETREES.get(script.m_AssemblyName, {}).get(script.m_ClassName, None) 29 | if nodes: 30 | export = obj.object_reader.read_typetree(nodes) 31 | else: 32 | export = obj.object_reader.read_typetree() 33 | 34 | if not export: 35 | extension = ".bin" 36 | export = obj.object_reader.raw_data 37 | else: 38 | extension = ".json" 39 | export = json.dumps(export, indent=4, ensure_ascii=False).encode("utf8", errors="surrogateescape") 40 | with open(f"{fp}{extension}", "wb") as f: 41 | f.write(export) 42 | return [(obj.assets_file, obj.object_reader.path_id)] 43 | 44 | EXPORT_TYPES = { 45 | ClassIDType.MonoBehaviour: exportMonoBehaviour, 46 | ClassIDType.TextAsset: exportTextAsset, 47 | } 48 | 49 | def _custom_load_folder(self, path): 50 | files_to_load = [] 51 | for root, dirs, files in self.fs.walk(path): 52 | for fname in files: 53 | low = fname.lower() 54 | if low.endswith(".unity3d"): 55 | files_to_load.append(self.fs.sep.join([root, fname])) 56 | self.load_files(files_to_load) 57 | 58 | Environment.load_folder = _custom_load_folder 59 | 60 | def export_obj(obj, destination, append_name=False, append_path_id=False): 61 | data = obj.read() 62 | 63 | extend = EXPORT_TYPES.get(obj.type) 64 | if extend is None: 65 | return [] 66 | 67 | fp = destination 68 | if append_name: 69 | name = data.m_Name if data.m_Name else data.object_reader.type.name 70 | fp = os.path.join(fp, name) 71 | 72 | base, ext = os.path.splitext(fp) 73 | if append_path_id: 74 | base = f"{base}_{data.object_reader.path_id}" 75 | return extend(data, base, ext) 76 | 77 | def extract_assets(source, target, include_types=None, ignore_first_dirs=0, append_path_id=False): 78 | print("Loading Unity Environment...") 79 | env = UnityPy.load(source) 80 | exported = [] 81 | 82 | type_order = list(EXPORT_TYPES.keys()) 83 | 84 | def order_key(item): 85 | if item[1].type in type_order: 86 | idx = type_order.index(item[1].type) 87 | else: 88 | idx = len(type_order) 89 | return idx 90 | 91 | print("Filtering and sorting items...") 92 | filtered_items = [] 93 | for item in env.container.items(): 94 | if item[1].m_PathID == 0: 95 | print(f"警告: 发现 m_PathID 为 0 的项目: {item[0]}") 96 | continue 97 | filtered_items.append(item) 98 | 99 | sorted_items = sorted(filtered_items, key=order_key) 100 | 101 | for obj_path, obj in tqdm(sorted_items, ncols=150): 102 | if include_types and obj.type.name not in include_types: 103 | continue 104 | parts = obj_path.split("/")[ignore_first_dirs:] 105 | 106 | filtered_parts = [] 107 | for p in parts: 108 | if p: 109 | filtered_parts.append(p) 110 | 111 | dest_dir = os.path.join(target, *filtered_parts) 112 | os.makedirs(os.path.dirname(dest_dir), exist_ok=True) 113 | exports = export_obj(obj, dest_dir, append_path_id=append_path_id) 114 | exported.extend(exports) 115 | 116 | return exported 117 | 118 | if __name__ == "__main__": 119 | parser = argparse.ArgumentParser() 120 | parser.add_argument("--src", default=r"E:\Game_Dataset\jp.co.craftegg.band\RAW") 121 | parser.add_argument("--dst", default=r"E:\Game_Dataset\jp.co.craftegg.band\EXP") 122 | args = parser.parse_args() 123 | 124 | source = os.path.join(args.src, "scenario") 125 | dst = os.path.join(args.dst, "Story") 126 | exported = extract_assets(source=source, target=dst, include_types=["MonoBehaviour"], ignore_first_dirs=5, append_path_id=True) 127 | 128 | source = os.path.join(args.src, "sound") 129 | dst = os.path.join(args.dst, "Sound") 130 | exported = extract_assets(source=source, target=dst, include_types=["TextAsset"], ignore_first_dirs=5, append_path_id=False) -------------------------------------------------------------------------------- /Unity/Fate Grand Order/CpkMaker.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Unity/Fate Grand Order/CpkMaker.dll -------------------------------------------------------------------------------- /Unity/Fate Grand Order/YACpkTool.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfloat16/Tools/d6a39bbec55ba8c0f6ed404f48c1f2d70e88e4ba/Unity/Fate Grand Order/YACpkTool.exe -------------------------------------------------------------------------------- /Unity/Fate Grand Order/dec_assetbundle.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import argparse 4 | from concurrent.futures import ThreadPoolExecutor, as_completed 5 | from dec_common import decrypt_and_decompress, get_interleaved_split 6 | from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn 7 | 8 | def args_parser(): 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument("--root", type=str, default=r"E:\Game_Dataset\com.aniplex.fategrandorder\RAW") 11 | parser.add_argument("--output", type=str, default=r"E:\Game_Dataset\com.aniplex.fategrandorder\DEC") 12 | return parser.parse_args() 13 | 14 | base_data, base_top, stage_data, stage_top = get_interleaved_split() 15 | 16 | columns = (SpinnerColumn(), BarColumn(bar_width=100), "[progress.percentage]{task.percentage:>6.2f}%", TimeElapsedColumn(), "•", TimeRemainingColumn(), TextColumn("[bold blue]{task.description}")) 17 | 18 | def read_version_info(root): 19 | storage_path = os.path.join(root, "3_AssetStorage.txt") 20 | with open(storage_path, "r", encoding="utf-8") as fp: 21 | lines = [ln.strip() for ln in fp if ln.strip()] 22 | data = [[field.strip() for field in line.split(',')] for line in lines[2:]] 23 | return data 24 | 25 | def other_home_building(data): 26 | bts = data.encode("utf‑8") 27 | home = bytearray(32) 28 | info = bytearray(32) 29 | 30 | for i, b in enumerate(bts[:32]): 31 | if i == 0: 32 | home[i] = b 33 | else: 34 | info[i] = b 35 | return bytes(home), bytes(info) 36 | 37 | # 有额外key 38 | def mouse_game4_with_key(data, key): 39 | home, info = other_home_building(key) 40 | array = decrypt_and_decompress(data, home, info, False) 41 | return array 42 | 43 | # 无额外key 44 | def mouse_game4(data): 45 | array = decrypt_and_decompress(data, base_data, base_top, False) 46 | buf = bytearray(array) 47 | for i in range(0, len(buf), 2): 48 | if i + 1 >= len(buf): 49 | break 50 | b, b2 = buf[i], buf[i + 1] 51 | buf[i] = b2 ^ 0xD2 52 | buf[i + 1] = b ^ 0xCE 53 | return bytes(buf) 54 | 55 | def process_ab(ab, root, output, prog, task_id): 56 | file_name = ab["FileName"] 57 | prog.update(task_id, description=f"{file_name}") 58 | ab_path = os.path.join(root, file_name + ".unity3d") 59 | with open(ab_path, "rb") as f: 60 | data = f.read() 61 | 62 | if ab["EXKey"]: 63 | array = mouse_game4_with_key(data, ab["EXKey"]) 64 | else: 65 | array = mouse_game4(data) 66 | 67 | output_path = os.path.join(output, file_name + ".unity3d") 68 | os.makedirs(os.path.dirname(output_path), exist_ok=True) 69 | with open(output_path, "wb") as f: 70 | f.write(array) 71 | 72 | prog.update(task_id, advance=1) 73 | 74 | def main(): 75 | args = args_parser() 76 | 77 | with open(os.path.join(args.root, "2_assetbundleKey.json"), "r", encoding="utf-8") as f: 78 | key_list = json.load(f) 79 | key_index = { key["id"]: key["decryptKey"] for key in key_list } 80 | 81 | txt_lines = read_version_info(args.root) 82 | result = [] 83 | for lines in txt_lines: 84 | if lines[4].endswith(".usm") or lines[4].endswith(".cpk.bytes"): 85 | continue 86 | elif len(lines) == 6: 87 | decryptkey = key_index.get(lines[5]) 88 | result.append({"FileName": lines[4], "EXKey": decryptkey}) 89 | else: 90 | result.append({"FileName": lines[4], "EXKey": None}) 91 | 92 | with Progress(*columns) as prog: 93 | task_id = prog.add_task("解密中...", total=len(result)) 94 | with ThreadPoolExecutor(max_workers=8) as executor: 95 | futures = [executor.submit(process_ab, ab, args.root, args.output, prog, task_id) for ab in result] 96 | for future in as_completed(futures): 97 | future.result() 98 | 99 | if __name__ == "__main__": 100 | main() -------------------------------------------------------------------------------- /Unity/Fate Grand Order/dec_common.py: -------------------------------------------------------------------------------- 1 | import bz2 2 | import gzip 3 | from cppdael import MODE_CBC, Pkcs7Padding, decrypt_unpad 4 | 5 | def get_interleaved_split(): 6 | s1 = "kzdMtpmzqCHAfx00saU1gIhTjYCuOD1JstqtisXsGYqRVcqrHRydj3k6vJCySu3g" 7 | s2 = "PFBs0eIuunoxKkCcLbqDVerU1rShhS276SAL3A8tFLUfGvtz3F3FFeKELIk3Nvi4" 8 | 9 | b1 = s1.encode('utf-8') 10 | b2 = s2.encode('utf-8') 11 | 12 | base_data = bytearray() 13 | base_top = bytearray() 14 | # 对 b2,每 4 字节为一组,组索引偶数放 base_data,奇数放 base_top 15 | for i in range(0, len(b2), 4): 16 | group = b2[i:i+4] 17 | if (i // 4) % 2 == 0: 18 | base_data.extend(group) 19 | else: 20 | base_top.extend(group) 21 | 22 | stage_data = bytearray() 23 | stage_top = bytearray() 24 | # 对 b1,按单字节索引,偶数位放 stage_data,奇数位放 stage_top 25 | for idx, byte in enumerate(b1): 26 | if idx % 2 == 0: 27 | stage_data.append(byte) 28 | else: 29 | stage_top.append(byte) 30 | 31 | # 转成只读的 bytes 返回 32 | return bytes(base_data), bytes(base_top), bytes(stage_data), bytes(stage_top) 33 | 34 | def decrypt_and_decompress(data, home, info, is_compress): 35 | decrypted = decrypt_unpad(MODE_CBC, 32, key=home, iv=info[:32], cipher=data, padding=Pkcs7Padding(32)) 36 | 37 | if is_compress: 38 | if decrypted.startswith(b"BZh"): 39 | return bz2.decompress(decrypted) 40 | if decrypted.startswith(b"\x1f\x8b\x08"): 41 | return gzip.decompress(decrypted) 42 | 43 | return decrypted -------------------------------------------------------------------------------- /Unity/Fate Grand Order/dec_pck.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import subprocess 3 | from pathlib import Path 4 | 5 | def main(): 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument("--root", type=str, default=r"E:\Game_Dataset\com.aniplex.fategrandorder\RAW\Audio") 8 | parser.add_argument("--output", type=str, default=r"E:\Game_Dataset\com.aniplex.fategrandorder\EXP\Audio_ENC") 9 | args = parser.parse_args() 10 | 11 | root = Path(args.root) 12 | output = Path(args.output) 13 | 14 | output.mkdir(parents=True, exist_ok=True) 15 | 16 | for cpk_file in root.rglob("*.cpk.bytes"): 17 | print(f"Extracting: {cpk_file}") 18 | try: 19 | subprocess.run(["Unity\Fate Grand Order\YACpkTool.exe", str(cpk_file), str(output)], check=True) 20 | except subprocess.CalledProcessError as e: 21 | print(f"Error extracting {cpk_file}: {e}") 22 | 23 | if __name__ == "__main__": 24 | main() -------------------------------------------------------------------------------- /Unity/Fate Grand Order/dec_script.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | import argparse 4 | from glob import glob 5 | from dec_common import decrypt_and_decompress, get_interleaved_split 6 | 7 | def args_parser(): 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("--root", type=str, default=r"E:\Game_Dataset\com.aniplex.fategrandorder\EXP\Story_ENC") 10 | parser.add_argument("--output", type=str, default=r"E:\Game_Dataset\com.aniplex.fategrandorder\EXP\Story_DEC") 11 | return parser.parse_args() 12 | 13 | base_data, base_top, stage_data, stage_top = get_interleaved_split() 14 | 15 | def mouse_game3(encoded_str): 16 | data = base64.b64decode(encoded_str) 17 | 18 | result = decrypt_and_decompress(data, stage_data, stage_top, True) 19 | if result is None: 20 | return None 21 | 22 | inverted = bytes((~b & 0xFF) for b in result) 23 | 24 | return inverted.decode('utf-8').rstrip('\x00') 25 | 26 | if __name__ == "__main__": 27 | args = args_parser() 28 | filelist = glob(args.root + r"\**\*.txt", recursive=True) 29 | 30 | for file in filelist: 31 | with open(file, "r", encoding="utf-8") as f: 32 | data = f.read() 33 | try: 34 | result = mouse_game3(data) 35 | except Exception as e: 36 | print(f"Error processing {file}: {e}") 37 | continue 38 | out_path = os.path.join(args.output, os.path.relpath(file, args.root)) 39 | os.makedirs(os.path.dirname(out_path), exist_ok=True) 40 | with open(out_path, "w", encoding="utf-8") as out_f: 41 | out_f.write(result) -------------------------------------------------------------------------------- /Unity/Fate Grand Order/downloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import time 4 | import hashlib 5 | import requests 6 | import argparse 7 | import binascii 8 | from dataclasses import dataclass 9 | from concurrent.futures import ThreadPoolExecutor, as_completed 10 | from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn 11 | 12 | def parser_args(): 13 | p = argparse.ArgumentParser() 14 | p.add_argument("--root", default=r"E:\Game_Dataset\com.aniplex.fategrandorder\RAW") 15 | p.add_argument("--workers", type=int, default=32) 16 | return p.parse_args() 17 | 18 | columns = (SpinnerColumn(), TextColumn("[bold blue]{task.description}"), BarColumn(bar_width=100), "[progress.percentage]{task.percentage:>6.2f}%", TextColumn("{task.completed}/{task.total}"), TimeElapsedColumn(), "•", TimeRemainingColumn()) 19 | 20 | @dataclass 21 | class AssetRow: 22 | rel_path: str # 文件相对路径(Movie/ops00100.usm) 23 | size: int # 字节大小 24 | crc32: int # 十进制 CRC32 25 | 26 | def category_of(path: str) -> str: 27 | bname = os.path.basename(path).lower() 28 | if bname.endswith(".cpk.bytes"): 29 | return "Audio" 30 | if bname.endswith(".usm"): 31 | return "Movie" 32 | return "Assetbundle" # 无后缀视为 ab 包 33 | 34 | def get_sha_name(name): 35 | sha1 = hashlib.sha1() 36 | sha1.update(name.encode("utf-8")) 37 | hashed = sha1.digest() 38 | return "".join(f"{b ^ 0xAA:02x}" for b in hashed) + ".bin" 39 | 40 | def bytes_to_gib(n): 41 | return round(n / 1024 ** 3, 2) 42 | 43 | def crc32_of_file(fp): 44 | with open(fp, "rb") as f: 45 | data = f.read() 46 | return binascii.crc32(data) & 0xFFFFFFFF 47 | 48 | def local_dest_path(root, row): 49 | rel = row.rel_path 50 | if category_of(rel) == "Assetbundle" and os.path.splitext(os.path.basename(rel))[1] == "": 51 | rel += ".unity3d" 52 | return os.path.join(root, rel) 53 | 54 | def read_version_info(root): 55 | storage_path = os.path.join(root, "3_AssetStorage.txt") 56 | with open(storage_path, "r", encoding="utf-8") as fp: 57 | lines = [ln.strip() for ln in fp if ln.strip()] 58 | 59 | ver_parts = lines[1].split(",") 60 | version_param = f"{ver_parts[0].lstrip('@')}_{ver_parts[1]}" 61 | return version_param, lines[2:] 62 | 63 | def read_folder_name(root): 64 | bundle_path = os.path.join(root, "2_assetbundle.json") 65 | with open(bundle_path, "r", encoding="utf-8") as fp: 66 | obj = json.load(fp) 67 | return obj["folderName"].rstrip("/") 68 | 69 | def parse_asset_rows(raw_lines): 70 | rows = [] 71 | for ln in raw_lines: 72 | parts = ln.split(",") 73 | if len(parts) < 5: 74 | continue 75 | rows.append(AssetRow(rel_path=parts[4], size=int(parts[2]), crc32=int(parts[3]))) 76 | return rows 77 | 78 | def build_url(base_root, ver_param, row: AssetRow): 79 | cat = category_of(row.rel_path) 80 | if cat == "Assetbundle": 81 | server_name = get_sha_name(row.rel_path.replace('/', '@') + '.unity3d') 82 | else: 83 | server_name = row.rel_path.replace("/", "_") 84 | return f"{base_root}{server_name}?v={ver_param}_{row.size}_{row.crc32}" 85 | 86 | def ensure_dir(path): 87 | folder = os.path.dirname(path) 88 | if folder and not os.path.exists(folder): 89 | os.makedirs(folder, exist_ok=True) 90 | 91 | def download(sess, url, dest): 92 | while True: 93 | try: 94 | r = sess.get(url) 95 | r.raise_for_status() 96 | ensure_dir(dest) 97 | with open(dest, "wb") as fp: 98 | fp.write(r.content) 99 | return 100 | except Exception as e: 101 | print(f"[W] {os.path.basename(dest)} Download failed: ({e})") 102 | time.sleep(1) 103 | 104 | def main(): 105 | args = parser_args() 106 | root = args.root 107 | workers = args.workers 108 | 109 | version_param, asset_lines = read_version_info(root) 110 | folder_name = read_folder_name(root) 111 | base_root = f"https://cdn.data.fate-go.jp/AssetStorages/{folder_name}/Android/" 112 | 113 | rows = parse_asset_rows(asset_lines) 114 | 115 | size_map = {"Audio": 0, "Movie": 0, "Assetbundle": 0} 116 | for r in rows: 117 | size_map[category_of(r.rel_path)] += r.size 118 | 119 | print( 120 | f"Audio payload : {bytes_to_gib(size_map['Audio']):>7.2f} GiB ({size_map['Audio']:,} bytes)\n" 121 | f"Movie payload : {bytes_to_gib(size_map['Movie']):>7.2f} GiB ({size_map['Movie']:,} bytes)\n" 122 | f"AssetBundle payload: {bytes_to_gib(size_map['Assetbundle']):>7.2f} GiB ({size_map['Assetbundle']:,} bytes)\n" 123 | f"Total payload : {bytes_to_gib(sum(size_map.values())):>7.2f} GiB ({sum(size_map.values()):,} bytes)\n" 124 | ) 125 | 126 | need_download = [] 127 | with Progress(*columns) as prog: 128 | crc_task = prog.add_task("Checking CRC", total=len(rows)) 129 | for r in rows: 130 | local_path = local_dest_path(root, r) 131 | if os.path.isfile(local_path) and crc32_of_file(local_path) == r.crc32: 132 | pass 133 | else: 134 | need_download.append((r, local_path)) 135 | prog.update(crc_task, advance=1) 136 | 137 | print(f"Need download: {len(need_download):,} / {len(rows):,}") 138 | 139 | if not need_download: 140 | print("All files are up-to-date.") 141 | return 142 | 143 | sess = requests.Session() 144 | sess.proxies.update({"http": "http://127.0.0.1:7897", "https": "http://127.0.0.1:7897"}) 145 | with Progress(*columns) as prog: 146 | task = prog.add_task("Downloading", total=len(need_download)) 147 | with ThreadPoolExecutor(max_workers=workers) as pool: 148 | futs = [pool.submit(download, sess, build_url(base_root, version_param, r), dest_path) for r, dest_path in need_download] 149 | for f in as_completed(futs): 150 | f.result() 151 | prog.update(task, advance=1) 152 | 153 | if __name__ == "__main__": 154 | main() -------------------------------------------------------------------------------- /Unity/Fate Grand Order/fetch_index.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import json 4 | import base64 5 | import msgpack 6 | import hashlib 7 | import argparse 8 | import requests 9 | from dec_common import decrypt_and_decompress, get_interleaved_split 10 | 11 | def args_parser(): 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument("--root", type=str, default=r"E:\Game_Dataset\com.aniplex.fategrandorder\RAW") 14 | return parser.parse_args() 15 | 16 | def get_md5_string(input_str): 17 | m = hashlib.md5() 18 | m.update((input_str + "pN6ds2Bg").encode('utf-8')) 19 | return m.hexdigest() 20 | 21 | def get_sha_name(name): 22 | sha1 = hashlib.sha1() 23 | sha1.update(name.encode('utf-8')) 24 | hashed = sha1.digest() 25 | return ''.join(f"{b ^ 0xAA:02x}" for b in hashed) + ".bin" 26 | 27 | def fetch_game_data(raw_path): 28 | url = "https://game.fate-go.jp/gamedata/top" 29 | resp = requests.get(url, params={"appVer": "0.0"}) 30 | data = resp.json() 31 | 32 | fail = data.get("response", [{}])[0].get("fail") 33 | if fail: 34 | action = fail.get("action") 35 | detail = fail.get("detail", "") 36 | if action == "app_version_up": 37 | new_ver = re.search(r"新ver.:(.*?)、", detail).group(1) 38 | print(f"new version: {new_ver}") 39 | resp = requests.get(url, params={"appVer": new_ver}) 40 | data = resp.json() 41 | else: 42 | raise Exception(detail) 43 | 44 | with open(raw_path, "w", encoding="utf-8") as f: 45 | json.dump(data, f, ensure_ascii=False) 46 | 47 | def fetch_assetbundle_key(raw_path, output_dir, key): 48 | with open(raw_path, encoding="utf-8") as f: 49 | raw = json.load(f) 50 | success = raw.get("response", [])[0].get("success") 51 | 52 | os.makedirs(output_dir, exist_ok=True) 53 | 54 | for field in ("assetbundle", "assetbundleKey"): 55 | b64_payload = success[field] 56 | 57 | payload = base64.b64decode(b64_payload) 58 | iv = payload[:32] 59 | ciphertext = payload[32:] 60 | 61 | key_bytes = key.encode("utf-8") 62 | plaintext = decrypt_and_decompress(ciphertext, key_bytes, iv, True) 63 | 64 | data = msgpack.unpackb(plaintext, raw=False) 65 | out_file_path = os.path.join(output_dir, f"2_{field}.json") 66 | with open(out_file_path, "w", encoding="utf-8") as out_f: 67 | json.dump(data, out_f, ensure_ascii=False, indent=2) 68 | 69 | def fetch_assetstorages(raw_path, output_dir): 70 | with open(raw_path, encoding="utf-8") as f: 71 | raw = json.load(f) 72 | version = raw["folderName"] 73 | url = f"https://cdn.data.fate-go.jp/AssetStorages/{version}Android/AssetStorage.txt" 74 | resp = requests.get(url) 75 | data = resp.text 76 | data = base64.b64decode(data) 77 | base_data, base_top, stage_data, stage_top = get_interleaved_split() 78 | data = decrypt_and_decompress(data, stage_data, stage_top, True) 79 | inverted = bytes((~b) & 0xFF for b in data) 80 | data = inverted.decode('utf-8').rstrip('\x00') 81 | with open(os.path.join(output_dir, "3_AssetStorage.txt"), "w", encoding="utf-8") as f: 82 | f.write(data) 83 | 84 | if __name__ == "__main__": 85 | args = args_parser() 86 | raw_path = os.path.join(args.root, "1_gamedata.json") 87 | fetch_game_data(raw_path) 88 | 89 | key = "W0Juh4cFJSYPkebJB9WpswNF51oa6Gm7" 90 | raw_path = os.path.join(args.root, "1_gamedata.json") 91 | fetch_assetbundle_key(raw_path, args.root, key) 92 | 93 | raw_path = os.path.join(args.root, "2_assetbundle.json") 94 | fetch_assetstorages(raw_path, args.root) -------------------------------------------------------------------------------- /Unity/Fate Grand Order/unpacker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from tqdm import tqdm 4 | 5 | import UnityPy 6 | from UnityPy.enums.ClassIDType import ClassIDType 7 | from UnityPy.environment import Environment 8 | 9 | def exportTextAsset(obj, fp, extension=".bytes"): 10 | with open(f"{fp}{extension}", "wb") as f: 11 | f.write(obj.m_Script.encode("utf-8", "surrogateescape")) 12 | return [(obj.assets_file, obj.object_reader.path_id)] 13 | 14 | EXPORT_TYPES = { 15 | ClassIDType.TextAsset: exportTextAsset, 16 | } 17 | 18 | def _custom_load_folder(self, path): 19 | files_to_load = [] 20 | for root, dirs, files in self.fs.walk(path): 21 | for fname in files: 22 | low = fname.lower() 23 | if low.endswith(".unity3d"): 24 | files_to_load.append(self.fs.sep.join([root, fname])) 25 | self.load_files(files_to_load) 26 | 27 | Environment.load_folder = _custom_load_folder 28 | 29 | def export_obj(obj, destination, append_name=False, append_path_id=False): 30 | data = obj.read() 31 | 32 | extend = EXPORT_TYPES.get(obj.type) 33 | if extend is None: 34 | return [] 35 | 36 | fp = destination 37 | if append_name: 38 | name = data.m_Name if data.m_Name else data.object_reader.type.name 39 | fp = os.path.join(fp, name) 40 | 41 | base, ext = os.path.splitext(fp) 42 | if append_path_id: 43 | base = f"{base}_{data.object_reader.path_id}" 44 | return extend(data, base, ext) 45 | 46 | 47 | def extract_assets(source, target, include_types=None, ignore_first_dirs=0, append_path_id=False): 48 | print("Loading Unity Environment...") 49 | env = UnityPy.load(source) 50 | exported = [] 51 | 52 | type_order = list(EXPORT_TYPES.keys()) 53 | 54 | def order_key(item): 55 | if item[1].type in type_order: 56 | idx = type_order.index(item[1].type) 57 | else: 58 | idx = len(type_order) 59 | return idx 60 | 61 | print("Filtering and sorting items...") 62 | filtered_items = [] 63 | for item in env.container.items(): 64 | if item[1].m_PathID == 0: 65 | print(f"警告: 发现 m_PathID 为 0 的项目: {item[0]}") 66 | continue 67 | filtered_items.append(item) 68 | 69 | sorted_items = sorted(filtered_items, key=order_key) 70 | 71 | for obj_path, obj in tqdm(sorted_items, ncols=150): 72 | if include_types and obj.type.name not in include_types: 73 | continue 74 | parts = obj_path.split("/")[ignore_first_dirs:] 75 | 76 | filtered_parts = [] 77 | for p in parts: 78 | if p: 79 | filtered_parts.append(p) 80 | 81 | dest_dir = os.path.join(target, *filtered_parts) 82 | os.makedirs(os.path.dirname(dest_dir), exist_ok=True) 83 | exports = export_obj(obj, dest_dir, append_path_id=append_path_id) 84 | exported.extend(exports) 85 | 86 | return exported 87 | 88 | if __name__ == "__main__": 89 | parser = argparse.ArgumentParser() 90 | parser.add_argument("--src", default=r"E:\Game_Dataset\com.aniplex.fategrandorder\DEC\ScriptActionEncrypt") 91 | parser.add_argument("--dst", default=r"E:\Game_Dataset\com.aniplex.fategrandorder\EXP\Story_ENC") 92 | parser.add_argument("--id", default=True) 93 | parser.add_argument("--ignore", type=int, default=3, metavar="N") 94 | parser.add_argument("--filter", nargs="+", default=["TextAsset"]) 95 | args = parser.parse_args() 96 | 97 | exported = extract_assets(source=args.src, target=args.dst, include_types=args.filter, ignore_first_dirs=args.ignore, append_path_id=args.id) -------------------------------------------------------------------------------- /Unity/Gakuen IDOLM@STER/script.py: -------------------------------------------------------------------------------- 1 | import json, re 2 | 3 | BRACKET_RE = re.compile(r'^\[(\w+)\s*(.*)]$') 4 | 5 | def unescape_braces(s: str) -> str: 6 | return s.replace(r'\{', '{').replace(r'\}', '}').replace(r'\[', '[').replace(r'\]', ']') 7 | 8 | def smart_split(s: str): 9 | parts, buf, depth = [], [], 0 10 | for ch in s: 11 | if ch == ' ' and depth == 0: 12 | if buf: parts.append(''.join(buf)); buf = [] 13 | else: 14 | buf.append(ch) 15 | if ch in '[{': depth += 1 16 | elif ch in ']}': depth -= 1 17 | if buf: parts.append(''.join(buf)) 18 | return parts 19 | 20 | def parse_value(raw: str): 21 | raw = unescape_braces(raw) 22 | if raw.startswith('{') and raw.endswith('}'): 23 | try: return json.loads(raw) 24 | except: return raw 25 | if raw.startswith('[') and raw.endswith(']'): 26 | # 把最外层 [] 剥掉后按内部分项继续解析 27 | inner = raw[1:-1].strip() 28 | if not inner: return [] 29 | items = [] 30 | # 粗分:第一层级的 ' item … ]' 组 31 | cur, depth = [], 0 32 | for ch in inner: 33 | cur.append(ch) 34 | if ch == '[': depth += 1 35 | elif ch == ']': depth -= 1 36 | if depth == 0 and ch == ']': 37 | items.append(''.join(cur)) 38 | cur = [] 39 | return [parse_line(item) for item in items] 40 | return raw 41 | 42 | def parse_line(line: str): 43 | m = BRACKET_RE.match(line.strip()) 44 | if not m: return {} 45 | cmd, body = m.groups() 46 | out = {'cmd': cmd} 47 | if not body: return out 48 | for field in smart_split(body): 49 | if '=' not in field: 50 | out.setdefault('flags', []).append(field) 51 | continue 52 | k, v = field.split('=', 1) 53 | out[k] = parse_value(v) 54 | return out 55 | 56 | if __name__ == '__main__': 57 | with open(r"E:\Game_Dataset\jp.co.bandainamcoent.BNEI0421\RAW\m_adventure\adv_cidol-amao-3-000_01.txt", 'r', encoding='utf-8') as f: 58 | lines = f.readlines() 59 | 60 | result = [] 61 | for line in lines: 62 | line = parse_line(line) 63 | result.append(line) 64 | 65 | pass -------------------------------------------------------------------------------- /Unity/Gakuen IDOLM@STER/tools/octo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Database { 4 | int32 revision = 1; 5 | repeated Data assetBundleList = 2; 6 | repeated string tagname = 3; 7 | repeated Data resourceList = 4; 8 | string urlFormat = 5; 9 | } 10 | 11 | message Data { 12 | int32 id = 1; 13 | string filepath = 2; 14 | string name = 3; 15 | int32 size = 4; 16 | uint32 crc = 5; 17 | int32 priority = 6; 18 | repeated int32 tagid = 7; 19 | repeated int32 dependencies = 8; 20 | State state = 9; 21 | string md5 = 10; 22 | string objectName = 11; 23 | uint64 generation = 12; 24 | int32 uploadVersionId = 13; 25 | 26 | enum State { 27 | NONE = 0; 28 | ADD = 1; 29 | UPDATE = 2; 30 | LATEST = 3; 31 | DELETE = 4; 32 | } 33 | } -------------------------------------------------------------------------------- /Unity/Gakuen IDOLM@STER/tools/octo_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: octo.proto 4 | # Protobuf Python Version: 4.25.7 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nocto.proto\"}\n\x08\x44\x61tabase\x12\x10\n\x08revision\x18\x01 \x01(\x05\x12\x1e\n\x0f\x61ssetBundleList\x18\x02 \x03(\x0b\x32\x05.Data\x12\x0f\n\x07tagname\x18\x03 \x03(\t\x12\x1b\n\x0cresourceList\x18\x04 \x03(\x0b\x32\x05.Data\x12\x11\n\turlFormat\x18\x05 \x01(\t\"\xae\x02\n\x04\x44\x61ta\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x10\n\x08\x66ilepath\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x0c\n\x04size\x18\x04 \x01(\x05\x12\x0b\n\x03\x63rc\x18\x05 \x01(\r\x12\x10\n\x08priority\x18\x06 \x01(\x05\x12\r\n\x05tagid\x18\x07 \x03(\x05\x12\x14\n\x0c\x64\x65pendencies\x18\x08 \x03(\x05\x12\x1a\n\x05state\x18\t \x01(\x0e\x32\x0b.Data.State\x12\x0b\n\x03md5\x18\n \x01(\t\x12\x12\n\nobjectName\x18\x0b \x01(\t\x12\x12\n\ngeneration\x18\x0c \x01(\x04\x12\x17\n\x0fuploadVersionId\x18\r \x01(\x05\">\n\x05State\x12\x08\n\x04NONE\x10\x00\x12\x07\n\x03\x41\x44\x44\x10\x01\x12\n\n\x06UPDATE\x10\x02\x12\n\n\x06LATEST\x10\x03\x12\n\n\x06\x44\x45LETE\x10\x04\x62\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'octo_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_DATABASE']._serialized_start=14 25 | _globals['_DATABASE']._serialized_end=139 26 | _globals['_DATA']._serialized_start=142 27 | _globals['_DATA']._serialized_end=444 28 | _globals['_DATA_STATE']._serialized_start=382 29 | _globals['_DATA_STATE']._serialized_end=444 30 | # @@protoc_insertion_point(module_scope) 31 | -------------------------------------------------------------------------------- /Unity/Heaven Burns Red/dec_script.py: -------------------------------------------------------------------------------- 1 | import os 2 | from Crypto.Cipher import AES 3 | from Crypto.Util.Padding import unpad 4 | from base64 import b64decode 5 | 6 | def decrypt_aes_base64(base64_data, key, iv): 7 | cipher_data = b64decode(base64_data) 8 | cipher = AES.new(key, AES.MODE_CBC, iv) 9 | decrypted_data = unpad(cipher.decrypt(cipher_data), AES.block_size) 10 | return decrypted_data 11 | 12 | def decrypt_files(input_folder, output_folder, key, iv): 13 | for root, _, files in os.walk(input_folder): 14 | for filename in files: 15 | if filename.endswith(".bytes"): 16 | input_file_path = os.path.join(root, filename) 17 | 18 | try: 19 | with open(input_file_path, 'rb') as file: 20 | file_content = file.read() 21 | base64_data = file_content.decode('utf-8') 22 | except UnicodeDecodeError: 23 | print(f"文件 {filename} 不是有效的 UTF-8 编码,跳过该文件。") 24 | continue 25 | 26 | try: 27 | decrypted_data = decrypt_aes_base64(base64_data, key, iv) 28 | 29 | # 保持目录结构 30 | relative_path = os.path.relpath(input_file_path, input_folder) 31 | output_file_path = os.path.join(output_folder, relative_path) 32 | output_file_path = output_file_path.replace(".bytes", ".lua") 33 | 34 | os.makedirs(os.path.dirname(output_file_path), exist_ok=True) 35 | 36 | with open(output_file_path, 'wb') as file: 37 | file.write(decrypted_data) 38 | print(f"解密并保存为 .lua 文件: {output_file_path}") 39 | except ValueError as e: 40 | print(f"解密失败: {filename}, 错误信息: {str(e)}") 41 | 42 | if __name__ == "__main__": 43 | input_folder = r"C:\Users\bfloat16\Desktop\hbr\TextAssets\Assets\Lua" 44 | output_folder = r"C:\Users\bfloat16\Desktop\hbr\TextAssets\Assets\Lua_dec" 45 | key = bytes([81, 103, 105, 88, 50, 97, 105, 33, 65, 35, 110, 98, 103, 58, 73, 111]) 46 | iv = bytes([119, 124, 81, 113, 74, 48, 65, 82, 117, 77, 84, 37, 115, 85, 112, 114]) 47 | 48 | if not os.path.exists(output_folder): 49 | os.makedirs(output_folder) 50 | 51 | decrypt_files(input_folder, output_folder, key, iv) -------------------------------------------------------------------------------- /Unity/Princess Connect! Re Dive/unpacker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from tqdm import tqdm 4 | 5 | import UnityPy 6 | from UnityPy.enums.ClassIDType import ClassIDType 7 | from UnityPy.environment import Environment 8 | 9 | def exportTextAsset(obj, fp, extension=".bytes"): 10 | with open(f"{fp}{extension}", "wb") as f: 11 | f.write(obj.m_Script.encode("utf-8", "surrogateescape")) 12 | return [(obj.assets_file, obj.object_reader.path_id)] 13 | 14 | EXPORT_TYPES = { 15 | ClassIDType.TextAsset: exportTextAsset, 16 | } 17 | 18 | UnityPy.config.FALLBACK_UNITY_VERSION = "2021.3.45f1" 19 | 20 | def _custom_load_folder(self, path): 21 | files_to_load = [] 22 | for root, dirs, files in self.fs.walk(path): 23 | for fname in files: 24 | low = fname.lower() 25 | if low.endswith(".unity3d") and "storydata" in low: 26 | files_to_load.append(self.fs.sep.join([root, fname])) 27 | self.load_files(files_to_load) 28 | 29 | Environment.load_folder = _custom_load_folder 30 | 31 | def export_obj(obj, destination, append_name=False, append_path_id=False): 32 | data = obj.read() 33 | 34 | extend = EXPORT_TYPES.get(obj.type) 35 | if extend is None: 36 | return [] 37 | 38 | fp = destination 39 | if append_name: 40 | name = data.m_Name if data.m_Name else data.object_reader.type.name 41 | fp = os.path.join(fp, name) 42 | 43 | base, ext = os.path.splitext(fp) 44 | if append_path_id: 45 | base = f"{base}_{data.object_reader.path_id}" 46 | return extend(data, base, ext) 47 | 48 | 49 | def extract_assets(source, target, include_types=None, ignore_first_dirs=0, append_path_id=False): 50 | print("Loading Unity Environment...") 51 | env = UnityPy.load(source) 52 | exported = [] 53 | 54 | type_order = list(EXPORT_TYPES.keys()) 55 | 56 | def order_key(item): 57 | if item[1].type in type_order: 58 | idx = type_order.index(item[1].type) 59 | else: 60 | idx = len(type_order) 61 | return idx 62 | 63 | print("Filtering and sorting items...") 64 | filtered_items = [] 65 | for item in env.container.items(): 66 | if item[1].m_PathID == 0: 67 | print(f"警告: 发现 m_PathID 为 0 的项目: {item[0]}") 68 | continue 69 | filtered_items.append(item) 70 | 71 | sorted_items = sorted(filtered_items, key=order_key) 72 | 73 | for obj_path, obj in tqdm(sorted_items, ncols=150): 74 | if include_types and obj.type.name not in include_types: 75 | continue 76 | if not obj_path.startswith('assets/_elementsresources/resources/storydata/data'): 77 | continue 78 | parts = obj_path.split("/")[ignore_first_dirs:] 79 | 80 | filtered_parts = [] 81 | for p in parts: 82 | if p: 83 | filtered_parts.append(p) 84 | 85 | dest_dir = os.path.join(target, *filtered_parts) 86 | os.makedirs(os.path.dirname(dest_dir), exist_ok=True) 87 | exports = export_obj(obj, dest_dir, append_path_id=append_path_id) 88 | exported.extend(exports) 89 | 90 | return exported 91 | 92 | if __name__ == "__main__": 93 | parser = argparse.ArgumentParser() 94 | parser.add_argument("--src", default=r"E:\Game_Dataset\jp.co.cygames.princessconnectredive\RAW") 95 | parser.add_argument("--dst", default=r"E:\Game_Dataset\jp.co.cygames.princessconnectredive\EXP\Story") 96 | parser.add_argument("--id", default=True) 97 | parser.add_argument("--ignore", type=int, default=5, metavar="N") 98 | parser.add_argument("--filter", nargs="+", default=["TextAsset"]) 99 | args = parser.parse_args() 100 | 101 | exported = extract_assets(source=args.src, target=args.dst, include_types=args.filter, ignore_first_dirs=args.ignore, append_path_id=args.id) -------------------------------------------------------------------------------- /Unity/THE IDOLM@STER CINDERELLA GIRLS/unpacker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import re 4 | import struct 5 | import argparse 6 | import lz4.block 7 | from tqdm import tqdm 8 | 9 | import UnityPy 10 | from UnityPy.enums.ClassIDType import ClassIDType 11 | from UnityPy.environment import Environment 12 | 13 | def exportTextAsset(obj, fp, extension=".bytes"): 14 | with open(f"{fp}{extension}", "wb") as f: 15 | f.write(obj.m_Script.encode("utf-8", "surrogateescape")) 16 | return [(obj.assets_file, obj.object_reader.path_id)] 17 | 18 | EXPORT_TYPES = { 19 | ClassIDType.TextAsset: exportTextAsset, 20 | } 21 | 22 | UnityPy.config.FALLBACK_UNITY_VERSION = "2021.3.45f1" 23 | 24 | def _custom_load_folder(self, path): 25 | files_to_load = [] 26 | for root, dirs, files in self.fs.walk(path): 27 | for fname in files: 28 | low = fname.lower() 29 | if low.endswith(".unity3d") and "story_storydata" in low: 30 | files_to_load.append(self.fs.sep.join([root, fname])) 31 | self.load_files(files_to_load) 32 | 33 | Environment.load_folder = _custom_load_folder 34 | 35 | reSplit = re.compile(r"(.*?([^\/\\]+?))\.split\d+") 36 | 37 | def _try_decompress(data): 38 | buf = io.BytesIO(data) 39 | num1, uncompressed_size, _, num2 = struct.unpack('