前言
本文Python代码与文章内容都遵循MIT协议
Copyright <2025> <Kinotern>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
仅供逆向汉化学习参考,不得用于其他非法用途
如果不想从零跑游戏制作对照参考表
需要要在论坛的搜索游戏,找到游戏下个模拟器版本的
拿一份模拟器版本的xp3,garbro解包它
没有模拟器副本的话自行跑游戏获取文件KrKrDump实时导出
请注意不是把原始xp3汇总扔游戏撞库
模拟器版本dump出来整理的打包xp3
其包含了游戏的全部文件,且未加密,用于恢复电脑版本的参考,免得二次重复性读取游戏还不全(ctrl的情况)
总好过一个个点选项跑游戏跑断腿吧
如果没有现成的参考副本那就老老实实跑一次游戏吧
本文重点在于制作映射表格以快速对CXDECExtracter提取的游戏文件分类重命名
上述其基本阐述了如何恢复的过程,也离不开大佬的探索
本文目的是制作详细教学方便小白入手
说来容易而实际上你要做的事情可不止这么简单
本文补充了大量的具体技术细节
1.使用Python汇总模拟器版本解包后游戏文件
源码如下
git记录
25/11/5
修复了GUI下汇总时窗口无反应
重新修复了汇总时导出的txt编码问题改为带BOM的utf-16le、
将默认导出文件名改为files
import os
import sys
import time
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
# 自然排序函数
def natural_sort_key(s):
import re
return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', s)]
# 核心文件提取逻辑
def extract_files(root_dir, output_path, recursive=True):
skip_files = {'.DS_Store', 'Thumbs.db', 'desktop.ini'}
skip_prefixes = ('~$', '.tmp', 'temp_')
total_count = 0
start_time = time.time()
try:
with open(output_path, "w", encoding="utf-16-le", newline="\n") as f:
# 显式写入UTF-16LE的BOM字符
f.write('\ufeff')
for root, dirs, files in os.walk(root_dir):
sorted_files = sorted(files, key=natural_sort_key)
for file in sorted_files:
if file in skip_files or file.startswith(skip_prefixes):
continue
f.write(file + "\n")
total_count += 1
total_time = time.time() - start_time
result_msg = f"成功提取 {total_count} 个文件\n"
result_msg += f"TXT保存路径:{output_path}\n"
result_msg += f"总耗时:{total_time:.1f} 秒"
return True, result_msg
except Exception as e:
error_msg = f"提取失败:{str(e)}\n请检查路径权限或文件名是否有特殊字符"
return False, error_msg
# 主GUI界面
def main_gui():
# 创建主窗口
root = tk.Tk()
root.title("文件列表提取工具")
root.geometry("550x220")
root.resizable(False, False) # 禁止调整窗口大小
# 界面组件
# 标题标签
title_label = ttk.Label(root, text="文件列表提取工具", font=("微软雅黑", 14, "bold"))
title_label.pack(pady=10)
# 进度提示
progress_label = ttk.Label(root, text="点击下方按钮选择文件夹开始提取", font=("微软雅黑", 10))
progress_label.pack(pady=5)
# 进度条
progress_bar = ttk.Progressbar(root, mode='indeterminate', length=450)
progress_bar.pack(pady=5, padx=20)
progress_bar.pack_forget() # 初始隐藏
# 后台提取线程函数
def extract_in_thread(target_dir, save_path):
# 执行提取操作
success, msg = extract_files(target_dir, save_path)
# 通过after方法线程安全地更新GUI
root.after(0, update_gui_after_extraction, success, msg)
# 提取完成后更新GUI
def update_gui_after_extraction(success, msg):
# 恢复界面状态
progress_bar.stop()
progress_bar.pack_forget()
# 显示结果
if success:
progress_label.config(text="提取完成!可点击按钮继续操作")
messagebox.showinfo("操作成功", msg)
else:
progress_label.config(text="提取失败!请检查错误信息")
messagebox.showerror("操作失败", msg)
# 开始提取按钮
def start_extraction():
# 选择目标文件夹
target_dir = filedialog.askdirectory(title="选择要提取文件列表的文件夹")
if not target_dir:
return
# 选择保存路径
save_path = filedialog.asksaveasfilename(
title="保存文件列表",
defaultextension=".txt",
filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
initialfile="files.txt"
)
if not save_path:
return
# 更新界面状态
progress_label.config(text="正在提取文件...请耐心等待")
progress_bar.pack(pady=5, padx=20)
progress_bar.start()
root.update_idletasks() # 立即刷新界面
# 创建并启动后台线程执行提取操作
extraction_thread = threading.Thread(
target=extract_in_thread,
args=(target_dir, save_path),
daemon=True # 设为守护线程,程序关闭时自动结束
)
extraction_thread.start()
start_btn = ttk.Button(
root,
text="选择文件夹并开始提取",
command=start_extraction,
width=30
)
start_btn.pack(pady=15)
# 退出按钮
exit_btn = ttk.Button(
root,
text="退出程序",
command=root.quit,
width=15
)
exit_btn.pack(pady=5)
# 运行主循环
root.mainloop()
# 主程序入口
if __name__ == "__main__":
# 命令行模式:如果传入路径参数,直接执行不显示GUI
if len(sys.argv) == 2:
target_dir = os.path.abspath(sys.argv[1])
if not os.path.exists(target_dir) or not os.path.isdir(target_dir):
print(f"错误:路径无效或不是文件夹 → {target_dir}")
sys.exit(1)
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
save_path = os.path.join(script_dir, "文件列表.txt")
print(f"命令行模式:开始提取 {target_dir}")
success, msg = extract_files(target_dir, save_path)
print(msg)
sys.exit(0 if success else 1)
# GUI模式:双击运行直接显示主界面
else:
try:
main_gui()
except Exception as e:
print(f"GUI启动失败:{str(e)}")
messagebox.showerror("错误", f"无法启动程序:{str(e)}")
sys.exit(1)
使用方法双击打开py用gui选择路径或命令行python 程序.py 文件夹路径
创建汇总文件的txt是以utf-16le形式储存,若想查看系统txt直接打开会出现问题
系统自带txt的是以utf-8格式读取,建议用subline text或VSCode来查看
采取utf-16l的文本编码的原因是游戏内部分文件是以日文来命名的
当使用utf-8格式txt输入cxdec撞库会以乱码的形式扔进去,无法制作获取映射表的
CXDEC的dll只负责把字符串转变为哈希你即使输入个1145141919810.png也会有hash
2.解密前准备条件
有足够的硬盘空间存储文件
还有基础的python使用认识
3.环境配置
电脑需有python
运行后缺少import库的自行pip install
实在不想输入命令也给你顺带编译好了py
4.Steam的KrKr游戏处理(潘多拉魔盒)
Steam版本的游戏提取需去除DRM后使用
这里就拿魔女的夜宴官中来演示吧
Steamless打开游戏exe

选1257项
点Unpack File
出现绿色字,提取成功
游戏目录会有一个叫SabbatOfTheWitch.exe.unpacked.exe
将Steam通用api复制到游戏目录替换

请注意测试游戏打开时显示这个
覆盖后仍
报错这个然后点确定后弹

或者
直接报Malformed exe/dll detected或krkrsteam.dll
通常情况下Steam官中脱壳后或HIKARI FIELD CLIENT下载的游戏程序拖入到提取程序会提示
原因是没绕过程序startup与bootstart的验证
例如
柚子社组乐队的新作,有软电池验证,通常用winHex删除KrKr2前面的软电池,直接删了没事,KrKrZ删除了前面的软电池程序的字节,从而让startup.tjs检测到了程序损坏
我下过一个学习版本的柚子社组乐队,业界知名逆向巨佬Dir-A制作的Version.dll好用但是这不和撞库用的version.dll重名了啊
windows的文件管理器也太难对付了......
所以遇到这种情况使用反编译器xdg32那些或者改bootstart也行,就可以不依赖dll做到撞库
工具箱的讲解用32xdg可以快速解决
实在不会做也有方案
拿网盘内提供的steamapi.dll加经xdg32处理过的游戏程序补丁,打上覆盖即可继续下一步
补丁针对柚子社还有部分krrkZ以hxv4加密为主的程序,下下来的补丁不放心可使用火绒等杀毒软件扫描
若盘内无支持游戏,请评论发issue,我要亲自把潘多拉的魔盒给炸了
5.提取
接下来使用CxdecExtractor拿到加密的文件夹映射表
https://github.com/YeLikesss/KrkrExtractForCxdecV2
将游戏程序拖到CxdecExtractor.exe

点第一个,把xp3一个个拖到窗口提取

提取时出现窗口未响应不要慌,等恢复相应状态即可
软件没有多线程,耐心等待


完成后关闭游戏再重新拖到提取程序打开
然后点第二个加载字符串Hash
游戏目录下的
StringHashDumper_Output会有DirectoryHash.log这个就是我们要的
有部分是空目录(如:%EmptyString%##YSig##94D4A97C61498621)留一个就行
最下面的一大段的重复行,不用管着先

6.撞库
前面提到的用python汇总到的文件名集合也就是文件列表.txt
将其改名为files.txt
https://github.com/YuriSizuku/GalgameReverse/blob/master/project/krkr/src/krkr_hxv4_dumphash.cpp
已编译好dll,要下的看到最后面的网盘工具箱
将version.dll与前面汇总拿到的files.txt复制到游戏目录
然后打开游戏后出现这个弹窗就是在撞库了
等待一会后
[src/krkr_hxv4_dumphash.cpp,216,calc_thread,I] try to calc names in dirs.txt [src/krkr_hxv4_dumphash.cpp,218,calc_thread,I] calculate finish, results in files_match.txt, dirs_match.txt
直接关掉命令窗口就可以快速关掉游戏了
即可在游戏目录找到files_match.txt与dirs_match.txt
撞库得到的信息是
001.共通-オナニーマスター.ks.scn,CAF2630573E58914755A99B3444995F3B7FF681F98D2220C7AF9370E5FA29F56
就这样子的就没问题了
7.程序自动恢复原始情况
基本思路是既然获取了映射
批量重命名然后将空文件夹命名为空再移到上级目录去
再根据文件映射表改名就可以用了
比较完善得版本
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文件批量重命名工具
根据files_match.txt和DirectoryHash.log进行文件/文件夹重命名
"""
import os
import shutil
import time
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
from pathlib import Path
import json
from datetime import datetime
import threading
class FileRenamerApp:
def __init__(self, root):
self.root = root
self.root.title("文件批量重命名工具")
self.root.geometry("900x700")
# 工作目录
self.working_dir = Path.cwd()
# 映射数据
self.file_mappings = {} # 哈希 -> 文件名
self.dir_mappings = {} # 哈希 -> 目录名
self.ready_dirs = [] # 需要处理的ready目录
# UI批处理优化
self.ui_update_queue = [] # UI更新队列
self.ui_update_timer = None # UI更新定时器
self.ui_batch_interval = 50 # 批处理间隔(毫秒)
# 性能监控
self.performance_stats = {
"file_rename_time": 0,
"dir_rename_time": 0,
"file_move_time": 0,
"total_time": 0,
"files_processed": 0,
"dirs_processed": 0
}
self.performance_log = [] # 性能日志记录
# 配置选项
self.skip_warnings = False # 是否跳过警告
self.setup_ui()
def setup_ui(self):
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置网格权重
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
# 工作目录选择
ttk.Label(main_frame, text="工作目录:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.dir_var = tk.StringVar(value=str(self.working_dir))
ttk.Entry(main_frame, textvariable=self.dir_var, width=60).grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5)
ttk.Button(main_frame, text="浏览", command=self.browse_directory).grid(row=0, column=2, padx=5)
# 文件映射导入
ttk.Label(main_frame, text="文件映射文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.file_map_var = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.file_map_var, width=60).grid(row=1, column=1, sticky=(tk.W, tk.E), padx=5)
ttk.Button(main_frame, text="选择", command=lambda: self.browse_file(self.file_map_var)).grid(row=1, column=2, padx=5)
# 目录映射导入
ttk.Label(main_frame, text="目录映射文件:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.dir_map_var = tk.StringVar()
ttk.Entry(main_frame, textvariable=self.dir_map_var, width=60).grid(row=2, column=1, sticky=(tk.W, tk.E), padx=5)
ttk.Button(main_frame, text="选择", command=lambda: self.browse_file(self.dir_map_var)).grid(row=2, column=2, padx=5)
# 按钮区域
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=10)
self.import_button = ttk.Button(button_frame, text="导入映射", command=self.load_mappings)
self.import_button.pack(side=tk.LEFT, padx=5)
self.preview_button = ttk.Button(button_frame, text="预览更改", command=self.preview_changes)
self.preview_button.pack(side=tk.LEFT, padx=5)
self.execute_button = ttk.Button(button_frame, text="开始恢复", command=self.execute_renaming)
self.execute_button.pack(side=tk.LEFT, padx=5)
self.clear_button = ttk.Button(button_frame, text="清空日志", command=self.clear_log)
self.clear_button.pack(side=tk.LEFT, padx=5)
# 跳过警告选项
self.skip_warnings_var = tk.BooleanVar(value=self.skip_warnings)
self.skip_warnings_check = ttk.Checkbutton(button_frame, text="跳过警告", variable=self.skip_warnings_var, command=self.update_skip_warnings)
self.skip_warnings_check.pack(side=tk.LEFT, padx=5)
# 状态信息
self.status_var = tk.StringVar(value="就绪")
ttk.Label(main_frame, textvariable=self.status_var).grid(row=4, column=0, columnspan=3, pady=5)
# 进度条
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var, maximum=100, length=400)
self.progress_bar.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
# 进度标签
self.progress_label = ttk.Label(main_frame, text="0%")
self.progress_label.grid(row=5, column=3, padx=5)
# 映射信息显示
notebook = ttk.Notebook(main_frame)
notebook.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
# 文件映射标签
file_frame = ttk.Frame(notebook)
self.file_tree = ttk.Treeview(file_frame, columns=("hash", "filename"), show="headings", height=10)
self.file_tree.heading("hash", text="哈希值")
self.file_tree.heading("filename", text="文件名")
self.file_tree.column("hash", width=300)
self.file_tree.column("filename", width=300)
scrollbar = ttk.Scrollbar(file_frame, orient=tk.VERTICAL, command=self.file_tree.yview)
self.file_tree.configure(yscrollcommand=scrollbar.set)
self.file_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
notebook.add(file_frame, text="文件映射")
# 目录映射标签
dir_frame = ttk.Frame(notebook)
self.dir_tree = ttk.Treeview(dir_frame, columns=("hash", "dirname"), show="headings", height=10)
self.dir_tree.heading("hash", text="哈希值")
self.dir_tree.heading("dirname", text="目录名")
self.dir_tree.column("hash", width=300)
self.dir_tree.column("dirname", width=300)
scrollbar2 = ttk.Scrollbar(dir_frame, orient=tk.VERTICAL, command=self.dir_tree.yview)
self.dir_tree.configure(yscrollcommand=scrollbar2.set)
self.dir_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar2.pack(side=tk.RIGHT, fill=tk.Y)
notebook.add(dir_frame, text="目录映射")
# 日志输出
ttk.Label(main_frame, text="操作日志:").grid(row=6, column=0, sticky=tk.W, pady=5)
self.log_text = scrolledtext.ScrolledText(main_frame, width=100, height=15)
self.log_text.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置网格权重
main_frame.rowconfigure(7, weight=1)
self.log("应用程序已启动")
def browse_directory(self):
directory = filedialog.askdirectory(initialdir=self.working_dir)
if directory:
self.working_dir = Path(directory)
self.dir_var.set(str(self.working_dir))
self.log(f"工作目录已设置为: {self.working_dir}")
def browse_file(self, var):
# 根据变量判断文件类型
if var == self.file_map_var:
filetypes = [("文本文件", "*.txt"), ("所有文件", "*.*")]
title = "选择文件映射文件"
elif var == self.dir_map_var:
filetypes = [("日志文件", "*.log"), ("所有文件", "*.*")]
title = "选择目录映射文件"
else:
filetypes = [("所有文件", "*.*")]
title = "选择文件"
filename = filedialog.askopenfilename(
initialdir=self.working_dir,
title=title,
filetypes=filetypes
)
if filename:
var.set(filename)
def log(self, message):
timestamp = datetime.now().strftime("%H:%M:%S")
log_message = f"[{timestamp}] {message}"
self.log_text.insert(tk.END, log_message + "\n")
self.log_text.see(tk.END)
print(log_message)
def clear_log(self):
self.log_text.delete(1.0, tk.END)
self.log("日志已清空")
self.log("日志已清空")
def load_mappings(self):
"""加载文件映射和目录映射"""
try:
# 清空现有数据
self.file_mappings.clear()
self.dir_mappings.clear()
self.ready_dirs.clear()
for item in self.file_tree.get_children():
self.file_tree.delete(item)
for item in self.dir_tree.get_children():
self.dir_tree.delete(item)
# 加载文件映射
file_map_path = self.file_map_var.get()
if file_map_path and os.path.exists(file_map_path):
try:
# 尝试UTF-16LE编码
with open(file_map_path, 'r', encoding='utf-16le') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if line and ',' in line:
parts = line.split(',', 1)
if len(parts) == 2:
filename = parts[0].strip()
hash_value = parts[1].strip()
# 文件映射:哈希值 -> 文件名
self.file_mappings[hash_value] = filename
self.file_tree.insert("", tk.END, values=(hash_value, filename))
self.log(f"已加载 {len(self.file_mappings)} 个文件映射 (UTF-16LE)")
except UnicodeDecodeError:
# UTF-16LE失败,尝试UTF-8
with open(file_map_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if line and ',' in line:
parts = line.split(',', 1)
if len(parts) == 2:
filename = parts[0].strip()
hash_value = parts[1].strip()
self.file_mappings[hash_value] = filename
self.file_tree.insert("", tk.END, values=(hash_value, filename))
self.log(f"已加载 {len(self.file_mappings)} 个文件映射 (UTF-8)")
except Exception as e:
messagebox.showerror("错误", f"读取文件映射时出错: {str(e)}")
self.log(f"错误: {str(e)}")
# 加载目录映射
dir_map_path = self.dir_map_var.get()
if dir_map_path and os.path.exists(dir_map_path):
seen_hashes = set() # 用于去重
try:
# 尝试UTF-16LE编码
with open(dir_map_path, 'r', encoding='utf-16le') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if line:
if '##YSig##' in line:
parts = line.split('##YSig##')
if len(parts) == 2:
dir_path = parts[0].rstrip('/')
hash_value = parts[1]
# 去重:如果已经处理过这个哈希值,跳过
if hash_value in seen_hashes:
continue
seen_hashes.add(hash_value)
# 处理特殊规则
if dir_path.strip() == '%EmptyString%' or dir_path.strip() == '':
dir_name = "" # 空字符串,不重命名目录
self.ready_dirs.append(hash_value)
elif dir_path == 'k2compat':
dir_name = "k2compat"
else:
# 使用完整路径作为目录名,保持父子关系
dir_name = dir_path
self.dir_mappings[hash_value] = dir_name
self.dir_tree.insert("", tk.END, values=(hash_value, dir_name))
self.log(f"已加载 {len(self.dir_mappings)} 个目录映射(已去重) (UTF-16LE)")
except UnicodeDecodeError:
# UTF-16LE失败,尝试UTF-8
with open(dir_map_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if line:
if '##YSig##' in line:
parts = line.split('##YSig##')
if len(parts) == 2:
dir_path = parts[0].rstrip('/')
hash_value = parts[1]
if hash_value in seen_hashes:
continue
seen_hashes.add(hash_value)
if dir_path == '%EmptyString%' or dir_path == '':
dir_name = "" # 空字符串,不重命名目录
self.ready_dirs.append(hash_value)
elif dir_path == 'k2compat':
dir_name = "k2compat"
else:
# 使用完整路径作为目录名,保持父子关系
dir_name = dir_path
self.dir_mappings[hash_value] = dir_name
self.dir_tree.insert("", tk.END, values=(hash_value, dir_name))
self.log(f"已加载 {len(self.dir_mappings)} 个目录映射(已去重) (UTF-8)")
except Exception as e:
messagebox.showerror("错误", f"读取目录映射时出错: {str(e)}")
self.log(f"错误: {str(e)}")
if self.ready_dirs:
self.log(f"找到 {len(self.ready_dirs)} 个需要处理的ready目录")
self.status_var.set(f"已加载 {len(self.file_mappings)} 文件映射, {len(self.dir_mappings)} 目录映射")
except Exception as e:
messagebox.showerror("错误", f"加载映射时出错: {str(e)}")
self.log(f"错误: {str(e)}")
def preview_changes(self):
"""预览将要进行的更改"""
if not self.file_mappings and not self.dir_mappings:
messagebox.showwarning("警告", "请先导入映射文件")
return
self.log("开始预览更改...")
# 预览文件重命名
file_changes = 0
for hash_value, filename in self.file_mappings.items():
# 查找哈希值对应的文件
# 文件可能位于data目录下的子目录中(根据哈希前2位分组)
hash_prefix = hash_value[:2].upper()
search_dir = self.working_dir / "data" / hash_prefix
if search_dir.exists():
for item in search_dir.iterdir():
if item.is_file() and item.name == hash_value:
self.log(f"文件: {item} -> {filename}")
file_changes += 1
break
else:
# 如果没有找到,可能在data目录的直接子目录中
for subdir in self.working_dir.glob("data/*"):
if subdir.is_dir():
for item in subdir.iterdir():
if item.is_file() and item.name == hash_value:
self.log(f"文件: {item} -> {filename}")
file_changes += 1
break
# 预览目录重命名
dir_changes = 0
for hash_value, dirname in self.dir_mappings.items():
target_dir = self.working_dir / "data" / hash_value
if target_dir.exists():
self.log(f"目录: {target_dir} -> {dirname}")
dir_changes += 1
else:
if not self.skip_warnings:
self.log(f"警告:未找到哈希值对应的文件目录: {hash_value}")
# 预览ready目录处理
ready_changes = 0
for hash_value in self.ready_dirs:
ready_dir = self.working_dir / "data" / hash_value
if ready_dir.exists():
for item in ready_dir.iterdir():
if item.is_file():
self.log(f"移动: {item} -> {ready_dir.parent / item.name}")
ready_changes += 1
if ready_changes > 0:
self.log(f"共 {ready_changes} 个文件需要从ready目录移动")
else:
self.log("没有需要从ready目录移动的文件")
def execute_renaming(self):
"""执行实际的重命名操作"""
if not self.file_mappings and not self.dir_mappings:
messagebox.showwarning("警告", "请先导入映射文件")
return
# 确认对话框
if not self.skip_warnings:
response = messagebox.askyesno("确认", "确定要执行重命名操作吗?此操作不可逆!")
if not response:
self.log("用户取消操作")
return
self.log("开始执行重命名操作...")
self.status_var.set("正在执行重命名...")
self.progress_var.set(0)
self.progress_label.config(text="0%")
# 禁用按钮
self.import_button.config(state="disabled")
self.preview_button.config(state="disabled")
self.execute_button.config(state="disabled")
# 在后台线程中执行
thread = threading.Thread(target=self._execute_renaming_thread, daemon=True)
thread.start()
def _execute_renaming_thread(self):
"""在后台线程中执行重命名"""
try:
start_time = time.time()
total_operations = len(self.file_mappings) + len(self.dir_mappings) + len(self.ready_dirs)
completed_operations = 0
# 1. 处理文件重命名
self.log("开始处理文件重命名...")
for hash_value, filename in self.file_mappings.items():
# 查找哈希值对应的文件
hash_prefix = hash_value[:2].upper()
search_dir = self.working_dir / "data" / hash_prefix
file_found = False
if search_dir.exists():
for item in search_dir.iterdir():
if item.is_file() and item.name == hash_value:
# 构建目标路径
target_path = self.working_dir / filename
target_path.parent.mkdir(parents=True, exist_ok=True)
# 移动/重命名文件
try:
shutil.move(str(item), str(target_path))
self.log(f"✓ 文件重命名: {item.name} -> {filename}")
file_found = True
break
except Exception as e:
self.log(f"✗ 文件重命名失败: {item.name} -> {filename}: {str(e)}")
if not file_found:
# 如果没有找到,可能在data目录的直接子目录中
for subdir in self.working_dir.glob("data/*"):
if subdir.is_dir():
for item in subdir.iterdir():
if item.is_file() and item.name == hash_value:
target_path = self.working_dir / filename
target_path.parent.mkdir(parents=True, exist_ok=True)
try:
shutil.move(str(item), str(target_path))
self.log(f"✓ 文件重命名: {item.name} -> {filename}")
file_found = True
break
except Exception as e:
self.log(f"✗ 文件重命名失败: {item.name} -> {filename}: {str(e)}")
if file_found:
break
if not file_found:
self.log(f"✗ 未找到哈希值对应的文件: {hash_value}")
completed_operations += 1
progress = (completed_operations / total_operations) * 100
self.progress_var.set(progress)
self.progress_label.config(text=f"{progress:.1f}%")
# 2. 处理目录重命名
self.log("开始处理目录重命名...")
ready_dirs_to_process = [] # 存储需要处理的ready目录
for hash_value, dirname in self.dir_mappings.items():
source_dir = self.working_dir / "data" / hash_value
if not source_dir.exists():
if not self.skip_warnings:
self.log(f"警告:未找到哈希值对应的文件目录: {hash_value}")
completed_operations += 1
continue
# 处理空目录名(%EmptyString%或空字符串)
if dirname == "":
# 添加到ready目录处理列表
ready_dirs_to_process.append((source_dir, self.working_dir))
self.log(f"✓ 标记为ready目录: {source_dir.name}")
completed_operations += 1
continue
# 构建目标目录路径
target_dir = self.working_dir / dirname
target_dir.parent.mkdir(parents=True, exist_ok=True)
# 检查目标目录是否存在
if target_dir.exists():
# 如果目标目录为空,删除它
try:
if not any(target_dir.iterdir()):
target_dir.rmdir()
self.log(f"✓ 删除空目标目录: {target_dir}")
except Exception as e:
self.log(f"✗ 无法删除目标目录: {target_dir}: {str(e)}")
# 重命名目录
try:
source_dir.rename(target_dir)
self.log(f"✓ 目录重命名: {source_dir.name} -> {dirname}")
except Exception as e:
self.log(f"✗ 目录重命名失败: {source_dir.name} -> {dirname}: {str(e)}")
completed_operations += 1
progress = (completed_operations / total_operations) * 100
self.progress_var.set(progress)
self.progress_label.config(text=f"{progress:.1f}%")
# 3. 处理ready目录(移动文件到父目录)
self.log("开始处理ready目录...")
for ready_dir, target_parent in ready_dirs_to_process:
if not ready_dir.exists():
self.log(f"✗ ready目录不存在: {ready_dir}")
completed_operations += 1
continue
# 移动所有文件到目标父目录
files_moved = 0
for item in ready_dir.iterdir():
if item.is_file():
target_path = target_parent / item.name
try:
shutil.move(str(item), str(target_path))
self.log(f"✓ 移动文件: {item.name} -> {target_parent.name}")
files_moved += 1
except Exception as e:
self.log(f"✗ 移动文件失败: {item.name}: {str(e)}")
# 如果目录为空,删除它
try:
if not any(ready_dir.iterdir()):
ready_dir.rmdir()
self.log(f"✓ 删除空ready目录: {ready_dir.name}")
else:
self.log(f"⚠ ready目录非空,保留: {ready_dir.name}")
except Exception as e:
self.log(f"✗ 无法删除ready目录: {ready_dir.name}: {str(e)}")
completed_operations += 1
progress = (completed_operations / total_operations) * 100
self.progress_var.set(progress)
self.progress_label.config(text=f"{progress:.1f}%")
# 4. 处理self.ready_dirs中的目录
self.log("开始处理额外的ready目录...")
for hash_value in self.ready_dirs:
ready_dir = self.working_dir / "data" / hash_value
if not ready_dir.exists():
self.log(f"✗ ready目录不存在: {ready_dir}")
completed_operations += 1
continue
# 移动所有文件到工作目录
files_moved = 0
for item in ready_dir.iterdir():
if item.is_file():
target_path = self.working_dir / item.name
try:
shutil.move(str(item), str(target_path))
self.log(f"✓ 移动文件: {item.name} -> 工作目录")
files_moved += 1
except Exception as e:
self.log(f"✗ 移动文件失败: {item.name}: {str(e)}")
# 如果目录为空,删除它
try:
if not any(ready_dir.iterdir()):
ready_dir.rmdir()
self.log(f"✓ 删除空ready目录: {ready_dir.name}")
else:
self.log(f"⚠ ready目录非空,保留: {ready_dir.name}")
except Exception as e:
self.log(f"✗ 无法删除ready目录: {ready_dir.name}: {str(e)}")
completed_operations += 1
progress = (completed_operations / total_operations) * 100
self.progress_var.set(progress)
self.progress_label.config(text=f"{progress:.1f}%")
# 完成
end_time = time.time()
total_time = end_time - start_time
self.log(f"重命名操作完成!总耗时: {total_time:.2f}秒")
self.status_var.set("操作完成")
self.progress_var.set(100)
self.progress_label.config(text="100%")
messagebox.showinfo("完成", f"重命名操作已完成!\n总耗时: {total_time:.2f}秒")
except Exception as e:
self.log(f"✗ 执行过程中发生错误: {str(e)}")
messagebox.showerror("错误", f"执行过程中发生错误: {str(e)}")
finally:
# 重新启用按钮
self.root.after(0, self._reenable_buttons)
def _reenable_buttons(self):
"""重新启用按钮"""
self.import_button.config(state="normal")
self.preview_button.config(state="normal")
self.execute_button.config(state="normal")
def update_skip_warnings(self):
"""更新跳过警告设置"""
self.skip_warnings = self.skip_warnings_var.get()
self.log(f"跳过警告: {'启用' if self.skip_warnings else '禁用'}")
if __name__ == "__main__":
root = tk.Tk()
app = FileRenamerApp(root)
root.mainloop()
8.文件补丁工具箱
☁️全部文件/
└── 📂软件/
├── 📂xdg32已处理程序/
│ ├── 📝1.解压密码0d000721
│ ├── 📝2.直接覆盖掉原始程序即可
│ ├── 📝3.可避免文件校验不通过
│ ├── 📝4.懒得自己逆也有弄好的版本
│ └── 📂补丁/
│ ├── 📂NekoWorks/
│ │ ├── 📦️Vol.1
│ │ ├── 📦️vol.2
│ │ ├── 📦️vol.3
│ │ ├── 📦️vol.4
│ │ ├── 📦️Extra
│ │ ├── 📦️After
│ │ └── 📂附带画集/
│ │ ├── 📦️vol.0
│ │ ├── 📦️vol.1
│ │ ├── 📦️vol.2
│ │ ├── 📦️vol.3
│ │ ├── 📦️vol.4
│ │ ├── 📦️Extra
│ │ ├── 📦️After
│ │ └── 📦️十周年
│ ├── 📂SisterPosition/
│ │ └── 📦️哥哥与妹妹的乡间生活
│ ├── 📂Yuzusoft/
│ │ ├── 📦️魔女的夜宴
│ │ ├── 📦️谜语小丑
│ │ ├── 📦️千恋万花
│ │ ├── 📦️星光咖啡馆
│ │ ├── 📦️天使纷扰
│ │ ├── 📦️新作-柠檬即兴曲(组乐队)名字待定
│ │ └── 📦️PARQUET
│ └── 📂Lose/
│ ├── 📦️LastRun
│ └── 📦️Pure Station
├── 📂CxdecExtractor/
│ └── 📦️CxdecExtractor.rar
├── 📂其他/
│ └── 📂PianoTrans/
│ └── 📦️PianoTrans-v1.0.7z
├── 📂Steamless/
│ ├── 🖻配置勾选1257.png
│ └── 📦️Steamless.v3.1.0.5.-.by.atom0s.zip
└── 📂注入/
├── 📂获取文件哈希对应/
│ ├── 📂源码/
│ │ └── 📦️krkr_hxv4_dumphash.zip
│ └── 📂编译后文件/
│ └── 📦️编译后文件.zip
└── 📂获取解密密钥/
└── 📦️获取解密密钥.7z
https://pan.baidu.com/s/12Q-cQh9v3eZjlhQ7O4-YFQ
提取码: 4btt

大佬我是少东西了吗?为啥说tee不是命令?

更改了version.dll好像无法运行游戏了#
files_match.txt只有60条是正常的吗
dirs_match.txt里面是空的
这些都是正常的吗?~
它却显示
目前是文件夹名已经还原状态
但似乎文件还在加密,是哪里出了问题吗?