3DMGAME 3DM首页 新闻中心 前瞻 | 评测 游戏库 热门 | 最新 攻略中心 攻略 | 秘籍 下载中心 游戏 | 汉化 购买正版 论坛

注册 登录

QQ登录

只需一步,快速开始

查看: 133|回复: 9
打印 上一主题 下一主题

[工具软件] 【控制台】星空物品查询器_V2.0 不用在各个网站找冷门代码了 自制小软件

[复制链接]

2

主题

401

帖子

521

积分

高级玩家

Rank: 4

贡献度
6
金元
4974
积分
521
精华
0
注册时间
2015-6-1
跳转到指定楼层
主题
发表于 2026-3-8 19:10 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
昨天在玩游戏的时候需要黄油来进行研究旧地球美食2,结果在度娘上搜出来一堆Galgame的选项简直一整个大无语,自己找的话还要一个一个网站的去搜基础代码,找了半天在代码堆里找到了我要的黄油代码,干脆直接基于python做了个小软件修修改改总算是也能用了,没有调用网络只使用本地数据库(如果有我没有录入的代码可以使用TXT手动录入格式为“代码”+ 空格2个/TAB +“名称”例:00225FC9        怪物装扮 )导入的txt文件内代码上一行加上sorting 太空服orXX是分类,软件会自动识别sorting关键词进行分类操作,下一行输入代码+名字就行,一个TXT内多分类也可以 之间空出一行继续添加sorting+空格+XX 就可以。


目前我自己在网络上搜索到的代码为2016条代码,包括技能,武器,弹药,头盔,太空服,背包,服饰,救援,食物,资源,矿物,书籍,杂志等,starfield_data.json这个文件就是建立的本地数据库。

除了自己搜索用之外别的小功能还有导出Excel功能,备份功能,收藏功能(右键菜单中,会置顶显示),鼠标左键双击条目自动复制到剪切板,下方代码生成工具箱可以为剪切板中的代码添加前缀比如复制“番茄00092B96”代码到-原始代码ID点击物品会为其添加player.additme前缀,右侧一件复制命令就可以直接在游戏内粘贴,自己加上数字就好。

软件基于python,源代码放到2楼有别的功能需求可以自己再改改

查毒 https://www.virscan.org/report/f ... 4218daf5d3da926ad2a
游客,如果您要查看本帖隐藏内容请回复


评分

1

查看全部评分

回复

使用道具 举报

2

主题

401

帖子

521

积分

高级玩家

Rank: 4

贡献度
6
金元
4974
积分
521
精华
0
注册时间
2015-6-1
舒服的沙发
 楼主| 发表于 2026-3-8 19:11 | 只看该作者
import os
import json
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
from pathlib import Path
import threading
import re
import sys
import shutil

# 尝试导入 openpyxl
try:
    from openpyxl import Workbook
    from openpyxl.styles import Font
    EXCEL_SUPPORT = True
except ImportError:
    EXCEL_SUPPORT = False

# --- 配置常量 ---
VERSION = "V2.0-Unique"
DB_FILE_NAME = "starfield_data.json"
WINDOW_TITLE = f" 星空物品查询器 {VERSION}"
FONT_FAMILY = "Microsoft YaHei UI"
FONT_SIZE = 10
CODE_FONT = ("Consolas", 10)

class StarfieldLiteApp:
    def __init__(self, root):
        self.root = root
        self.root.title(WINDOW_TITLE)
        self.root.geometry("950x700")
        
        # 高 DPI 支持
        try:
            from ctypes import windll
            windll.shcore.SetProcessDpiAwareness(1)
        except:
            pass

        self.database = {}
        self.filtered_data = []
        
        # 【新增】代码反向索引:{ "00035A48": "战刀" }
        # 用于在导入时快速判断代码是否已存在
        self.code_index = {}

        # 智能获取运行目录
        if getattr(sys, 'frozen', False):
            self.base_dir = Path(sys.executable).parent.resolve()
        else:
            self.base_dir = Path(__file__).parent.resolve()
            
        self.db_path = self.base_dir / DB_FILE_NAME

        self.setup_styles()
        self.create_ui()
        self.load_database()

    def setup_styles(self):
        style = ttk.Style()
        if 'vista' in style.theme_names():
            style.theme_use('vista')
        elif 'clam' in style.theme_names():
            style.theme_use('clam')
            
        style.configure(".", font=(FONT_FAMILY, FONT_SIZE))
        style.configure("Treeview", rowheight=28, font=(FONT_FAMILY, 10))
        style.configure("Treeview.Heading", font=(FONT_FAMILY, 10, "bold"))
        style.configure("Status.TLabel", foreground="#555555", background="#f0f0f0")
        style.configure("Tool.TButton", padding=(10, 5))

    def create_ui(self):
        # === 顶部:搜索与操作 ===
        top_frame = ttk.Frame(self.root, padding="10")
        top_frame.pack(side=tk.TOP, fill=tk.X)
        
        search_frame = ttk.LabelFrame(top_frame, text=" 搜索 (名称/代码/分类)", padding=5)
        search_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *args: self.perform_search())
        
        self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, font=(FONT_FAMILY, 12))
        self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        self.search_entry.focus_set()
        
        file_frame = ttk.Frame(top_frame)
        file_frame.pack(side=tk.RIGHT)
        
        ttk.Button(file_frame, text=" 手动加载库", command=self.manual_load_database).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_frame, text=" 智能导入 TXT", command=self.import_data_threaded).pack(side=tk.LEFT, padx=2)
        
        if EXCEL_SUPPORT:
            ttk.Button(file_frame, text=" 导出 Excel", command=self.export_to_excel).pack(side=tk.LEFT, padx=2)
        else:
            ttk.Button(file_frame, text="❌ 无 Excel", command=self.show_excel_error).pack(side=tk.LEFT, padx=2)
            
        ttk.Button(file_frame, text=" 导出备份", command=self.export_data).pack(side=tk.LEFT, padx=2)
        ttk.Button(file_frame, text="️ 清空", command=self.clear_data).pack(side=tk.LEFT, padx=2)

        # === 中部:列表展示区 ===
        list_frame = ttk.Frame(self.root, padding="10")
        list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
        
        columns = ("cat", "name", "code")
        self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse")
        
        self.tree.heading("cat", text="分类", anchor="center")
        self.tree.heading("name", text="物品名称")
        self.tree.heading("code", text="代码 (ID)", anchor="center")
        
        self.tree.column("cat", width=90, minwidth=70, anchor="center")
        self.tree.column("name", width=350, minwidth=200)
        self.tree.column("code", width=150, minwidth=100, anchor="center")
        
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.tree.bind("<Double-1>", self.on_double_click)
        self.tree.bind("<Button-3>", self.on_right_click)

        # === 下部:代码工具箱 ===
        tool_frame = ttk.LabelFrame(self.root, text="️ 代码生成工具箱", padding=10)
        tool_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
        
        row1 = ttk.Frame(tool_frame)
        row1.pack(fill=tk.X, pady=(0, 5))
        
        ttk.Label(row1, text="原始代码 ID:").pack(side=tk.LEFT, padx=(0, 5))
        self.input_raw = ttk.Entry(row1, width=20, font=CODE_FONT)
        self.input_raw.pack(side=tk.LEFT, padx=(0, 10))
        
        ttk.Button(row1, text=" 物品", command=lambda: self.generate_code("player.additem")).pack(side=tk.LEFT, padx=2)
        ttk.Button(row1, text="⭐ 技能", command=lambda: self.generate_code("player.addperk")).pack(side=tk.LEFT, padx=2)
        ttk.Button(row1, text=" 属性", command=lambda: self.generate_code("player.setav")).pack(side=tk.LEFT, padx=2)
        
        row2 = ttk.Frame(tool_frame)
        row2.pack(fill=tk.X)
        
        ttk.Label(row2, text="生成命令:").pack(side=tk.LEFT, padx=(0, 5))
        self.input_result = ttk.Entry(row2, width=50, font=CODE_FONT, state="readonly")
        self.input_result.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
        
        self.btn_copy_tool = ttk.Button(row2, text=" 复制命令", command=self.copy_tool_result)
        self.btn_copy_tool.pack(side=tk.RIGHT)

        # === 底部:状态栏 ===
        self.status_var = tk.StringVar()
        self.status_var.set(f"就绪 | 版本 {VERSION}")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, style="Status.TLabel")
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)

    # --- 核心逻辑 ---

    def load_database(self):
        if not self.db_path.exists():
            self.status_var.set(f"未找到数据库 | 版本 {VERSION} (请导入 TXT)")
            self.database = {}
            self.code_index = {}
            self.update_status()
            return
        
        try:
            with open(self.db_path, 'r', encoding='utf-8') as f:
                content = f.read().strip()
                self.database = json.loads(content) if content else {}
            
            # 【关键】加载时重建代码索引
            self.rebuild_code_index()
            
            self.perform_search()
            self.status_var.set(f"已加载:{len(self.database)}条数据 | 版本 {VERSION}")
        except Exception as e:
            messagebox.showerror("数据库错误", f"读取失败:\n{e}\n建议清空或重新导入。")
            self.database = {}
            self.code_index = {}
            self.update_status()

    def rebuild_code_index(self):
        """遍历数据库,重建 code -> name 映射"""
        self.code_index = {}
        for name, info in self.database.items():
            code = info.get('code', '').upper()
            if code:
                # 如果发现有多个名字对应同一个代码(脏数据),保留第一个遇到的
                if code not in self.code_index:
                    self.code_index[code] = name

    def manual_load_database(self):
        file_path = filedialog.askopenfilename(
            title="选择数据库文件 (JSON)",
            filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
            initialdir=self.base_dir
        )
        if not file_path: return
            
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                temp_data = json.load(f)
            
            count = len(temp_data)
            if not messagebox.askyesno("确认加载", f"找到 {count} 条数据。\n确定加载并设为默认?\n注意:这将覆盖当前内存中的所有数据。"):
                return

            shutil.copy(file_path, self.db_path)
            self.database = temp_data
            self.rebuild_code_index()
            self.perform_search()
            self.status_var.set(f"✅ 已加载 {count} 条 | 版本 {VERSION}")
            messagebox.showinfo("成功", "数据库加载成功!")
            
        except json.JSONDecodeError:
            messagebox.showerror("格式错误", "文件不是有效的 JSON。")
        except Exception as e:
            messagebox.showerror("加载失败", str(e))

    def save_database(self):
        try:
            with open(self.db_path, 'w', encoding='utf-8') as f:
                json.dump(self.database, f, ensure_ascii=False, indent=2)
            # 保存后不需要重建索引,因为只是修改现有数据,除非新增了条目
            # 但为了保险,如果在导入过程中调用了 save,索引已经在内存中维护好了
            self.update_status()
        except Exception as e:
            messagebox.showerror("保存错误", str(e))

    def normalize_category(self, cat_str):
        cat_str = cat_str.strip()
        aliases = {
            "weapon": "武器", "suit": "太空服", "armor": "太空服",
            "helmet": "头盔", "bag": "背包", "skill": "技能",
            "perk": "技能", "stat": "属性", "ammo": "弹药",
            "throwable": "投掷物", "note": "笔记", "resource": "资源",
            "misc": "杂项", "rescue": "救援", "med": "救援"
        }
        lower_cat = cat_str.lower()
        if lower_cat in aliases:
            return aliases[lower_cat]
        return cat_str if cat_str else "杂项"

    def perform_search(self):
        query = self.search_var.get().lower()
        self.filtered_data = []
        
        for name, info in self.database.items():
            cat = info.get('cat', '杂项')
            code = info['code']
            fav = info.get('fav', False)
            
            match = not query or (query in name.lower() or query in code.lower() or query in cat.lower())
            
            if match:
                self.filtered_data.append({"name": name, "code": code, "cat": cat, "fav": fav})
        
        self.filtered_data.sort(key=lambda x: (not x['fav'], x['cat'], x['name']))
        self.refresh_tree()
        self.update_status()

    def refresh_tree(self):
        self.tree.delete(*self.tree.get_children())
        for item in self.filtered_data:
            name = item['name']
            code = item['code']
            cat = item['cat']
            fav = item['fav']
            
            display_name = f"⭐ {name}" if fav else name
            display_cat = f"[{cat}]"
            self.tree.insert("", tk.END, values=(display_cat, display_name, code))

    def update_status(self):
        total = len(self.database)
        current = len(self.filtered_data)
        self.status_var.set(f"就绪 | 版本 {VERSION} | 总数:{total} | 结果:{current}")

    # --- 交互事件 ---

    def on_double_click(self, event):
        selection = self.tree.selection()
        if not selection: return
        code = self.tree.item(selection[0])['values'][2]
        self.copy_to_clipboard(str(code))
        self.status_var.set(f"✅ 已复制:{code}")

    def on_right_click(self, event):
        item_id = self.tree.identify_row(event.y)
        if not item_id: return
        
        self.tree.selection_set(item_id)
        item = self.tree.item(item_id)
        vals = item['values']
        
        real_cat = vals[0].strip("[]")
        name_raw = vals[1]
        code = vals[2]
        
        is_fav = name_raw.startswith("⭐ ")
        real_name = name_raw[2:] if is_fav else name_raw

        menu = tk.Menu(self.root, tearoff=0)
        menu.add_command(label=" 复制代码", command=lambda: self.copy_to_clipboard(code))
        menu.add_command(label="✏️ 修改代码", command=lambda: self.edit_item(real_name, 'code'))
        menu.add_command(label="️ 修改分类", command=lambda: self.edit_item(real_name, 'cat'))
        menu.add_separator()
        fav_text = "❌ 取消收藏" if is_fav else "⭐ 加入收藏"
        menu.add_command(label=fav_text, command=lambda: self.toggle_favorite(real_name))
        menu.add_separator()
        menu.add_command(label="️ 删除此项", command=lambda: self.delete_item(real_name), foreground="red")
        
        menu.post(event.x_root, event.y_root)

    def edit_item(self, name, field):
        if name not in self.database: return
        current_val = self.database[name].get(field, '杂项' if field == 'cat' else '')
        prompt = f"请输入新的{field}:" if field == 'code' else "请输入新的分类 (如:武器):"
        
        new_val = simpledialog.askstring(f"修改 {field}", prompt, initialvalue=current_val)
        if new_val and new_val.strip():
            old_code = self.database[name].get('code', '')
            
            if field == 'code':
                # 如果修改了代码,需要更新索引
                new_code = new_val.strip().upper()
                if new_code != old_code:
                    # 移除旧索引
                    if old_code in self.code_index and self.code_index[old_code] == name:
                        del self.code_index[old_code]
                    # 添加新索引(如果新代码没被占用)
                    if new_code in self.code_index:
                        messagebox.showwarning("代码冲突", f"代码 {new_code} 已存在于库中(属于 {self.code_index[new_code]})。\n已强制更新当前物品的代码。")
                    self.code_index[new_code] = name
            
            if field == 'cat':
                new_val = self.normalize_category(new_val)
               
            self.database[name][field] = new_val
            self.save_database()
            self.perform_search()

    def toggle_favorite(self, name):
        if name not in self.database: return
        self.database[name]['fav'] = not self.database[name].get('fav', False)
        self.save_database()
        self.perform_search()

    def delete_item(self, name):
        if messagebox.askyesno("确认删除", f"确定删除「{name}」?"):
            code = self.database[name].get('code', '')
            # 从索引中移除
            if code in self.code_index and self.code_index[code] == name:
                del self.code_index[code]
            
            del self.database[name]
            self.save_database()
            self.perform_search()

    def copy_to_clipboard(self, text):
        self.root.clipboard_clear()
        self.root.clipboard_append(text)
        self.root.update()

    def generate_code(self, prefix):
        raw = self.input_raw.get().strip()
        if not raw:
            messagebox.showwarning("提示", "请先输入代码 ID")
            return
        clean_raw = re.sub(r'^player\.\w+\s+', '', raw)
        result = f"{prefix} {clean_raw}"
        self.input_result.config(state="normal")
        self.input_result.delete(0, tk.END)
        self.input_result.insert(0, result)
        self.input_result.config(state="readonly")

    def copy_tool_result(self):
        val = self.input_result.get()
        if val:
            self.copy_to_clipboard(val)
            self.status_var.set(f"✅ 已复制:{val}")

    # --- 导入/导出逻辑 (核心修改部分) ---

    def import_data_threaded(self):
        file_path = filedialog.askopenfilename(title="选择数据文件 (TXT)", filetypes=[("Text Files", "*.txt")])
        if not file_path: return
        
        win = tk.Toplevel(self.root)
        win.title("正在智能导入...")
        win.geometry("300x100")
        win.transient(self.root)
        win.grab_set()
        
        ttk.Label(win, text="正在解析文件...\n检测 Sorting 标记").pack(pady=20)
        pb = ttk.Progressbar(win, mode='indeterminate')
        pb.pack(fill=tk.X, padx=20)
        pb.start(10)
        
        def worker():
            try:
                stats = self.process_import_file_smart(file_path)
                self.root.after(0, lambda: self.finish_import(win, stats))
            except Exception as e:
                self.root.after(0, lambda: messagebox.showerror("导入失败", str(e)))
                self.root.after(0, win.destroy)
        
        threading.Thread(target=worker, daemon=True).start()

    def process_import_file_smart(self, path):
        """
        智能导入逻辑:
        1. 检测 'sorting 分类名' 行来切换当前分类。
        2. 检测代码唯一性,如果代码已存在,跳过该行(防止重复)。
        """
        new_count = 0
        dup_code_count = 0
        dup_name_count = 0
        current_category = "杂项"
        categories_found = set()
        
        with open(path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        
        if not lines:
            return {"new": 0, "dup_code": 0, "dup_name": 0, "cats": []}

        for line in lines:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            
            # 【策略 1】检测 Sorting 标记
            # 匹配 "sorting 武器" 或 "Sorting:头盔" 等格式
            sort_match = re.match(r'^sorting\s*[::]?\s*(.+)$', line, re.IGNORECASE)
            if sort_match:
                current_category = self.normalize_category(sort_match.group(1))
                categories_found.add(current_category)
                continue
            
            # 【策略 2】解析数据行
            # 支持 Tab 或多个空格分隔
            parts = re.split(r'\t+|\s{2,}', line)
            
            if len(parts) >= 2:
                code = parts[0].strip().upper() # 统一转大写方便比对
                name = parts[1].strip()
               
                if not code or not name:
                    continue

                # 【策略 3】代码唯一性检查
                if code in self.code_index:
                    # 代码已存在
                    existing_name = self.code_index[code]
                    if existing_name == name:
                        # 完全重复(名和码都一样)
                        dup_code_count += 1
                    else:
                        # 代码相同但名字不同(如:战刀 vs 战斗刀)
                        # 策略:保留旧数据,跳过新数据,计数为“代码重复”
                        dup_code_count += 1
                    continue
               
                # 【策略 4】名字重复检查(可选,防止同名不同码,但通常代码唯一更重要)
                # 这里我们主要依赖代码唯一性。如果名字重复但代码不同,视为不同物品(可能是翻译差异)
                # 如果希望名字也唯一,可以解开下面注释
                """
                if name in self.database:
                    dup_name_count += 1
                    continue
                """

                # 入库
                self.database[name] = {
                    "code": code,
                    "cat": current_category,
                    "fav": False
                }
                # 更新索引
                self.code_index[code] = name
                new_count += 1
        
        self.save_database()
        return {
            "new": new_count,
            "dup_code": dup_code_count,
            "dup_name": dup_name_count,
            "cats": sorted(list(categories_found))
        }

    def finish_import(self, win, stats):
        win.destroy()
        
        cat_list = ", ".join(stats['cats']) if stats['cats'] else "无新分类"
        msg = (f"✅ 智能导入完成!\n\n"
               f" 涉及分类:【{cat_list}】\n"
               f"➕ 新增物品:{stats['new']}\n"
               f"⚠️ 跳过代码重复:{stats['dup_code']} (代码已存在,保留旧数据)\n")
               # f"⚠️ 跳过名字重复:{stats['dup_name']}") # 如果启用了名字检查
        
        messagebox.showinfo("导入结果", msg)
        self.perform_search()
        self.status_var.set(f"导入成功 | 新增 {stats['new']} 条 | 版本 {VERSION}")

    def export_data(self):
        path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text Files", "*.txt")])
        if not path: return
        try:
            grouped = {}
            for n, i in self.database.items():
                c = i.get('cat', '杂项')
                grouped.setdefault(c, []).append((n, i))
            
            with open(path, 'w', encoding='utf-8') as f:
                f.write("# Starfield Database Backup\n")
                for c, items in sorted(grouped.items()):
                    f.write(f"sorting {c}\n") # 导出时也带上 sorting 标记,方便下次导入
                    for n, i in items:
                        f.write(f"{i['code']}\t{n}\n")
                    f.write("\n")
            messagebox.showinfo("成功", f"文本备份已导出:\n{path}")
        except Exception as e:
            messagebox.showerror("错误", str(e))

    def show_excel_error(self):
        messagebox.showwarning("缺少依赖",
            "未检测到 'openpyxl' 库。\n\n"
            "如需导出 Excel,请在命令行运行:\n"
            "pip install openpyxl\n"
            "然后重新打包程序。")

    def export_to_excel(self):
        if not EXCEL_SUPPORT:
            self.show_excel_error()
            return
            
        path = filedialog.asksaveasfilename(
            defaultextension=".xlsx",
            filetypes=[("Excel Files", "*.xlsx")],
            initialfile=f"Starfield_Data_{VERSION}.xlsx"
        )
        if not path: return
        
        try:
            wb = Workbook()
            ws = wb.active
            ws.title = "Items"
            
            headers = ["分类", "物品名称", "代码 ID", "收藏"]
            ws.append(headers)
            
            for name, info in self.database.items():
                cat = info.get('cat', '杂项')
                code = info['code']
                fav = "是" if info.get('fav', False) else "否"
                ws.append([cat, name, code, fav])
            
            for col in ws.columns:
                max_length = 0
                column = col[0].column_letter
                for cell in col:
                    if cell.value:
                        max_length = max(max_length, len(str(cell.value)))
                ws.column_dimensions[column].width = min(max_length + 2, 50)
            
            bold_font = Font(bold=True)
            for cell in ws[1]:
                cell.font = bold_font
               
            wb.save(path)
            messagebox.showinfo("成功", f"Excel 已导出:\n{path}\n共 {len(self.database)} 条数据。")
            
        except Exception as e:
            messagebox.showerror("导出失败", str(e))

    def clear_data(self):
        if messagebox.askyesno("高危警告", "确定要清空所有数据吗?\n此操作不可恢复!"):
            self.database = {}
            self.code_index = {}
            self.save_database()
            self.perform_search()
            messagebox.showinfo("已清空", "数据库已重置。")

if __name__ == "__main__":
    root = tk.Tk()
    app = StarfieldLiteApp(root)
    root.mainloop()
回复 支持 反对

使用道具 举报

2

主题

401

帖子

521

积分

高级玩家

Rank: 4

贡献度
6
金元
4974
积分
521
精华
0
注册时间
2015-6-1
硬硬的板凳
 楼主| 发表于 2026-3-8 19:13 | 只看该作者
import os
import json
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
from pathlib import Path
import threading
import re
import sys
import shutil

# 尝试导入 openpyxl
try:
    from openpyxl import Workbook
    from openpyxl.styles import Font
    EXCEL_SUPPORT = True
except ImportError:
    EXCEL_SUPPORT = False

# --- 配置常量 ---
VERSION = "V2.0-Unique"
DB_FILE_NAME = "starfield_data.json"
WINDOW_TITLE = f" 星空物品查询器 {VERSION}"
FONT_FAMILY = "Microsoft YaHei UI"
FONT_SIZE = 10
CODE_FONT = ("Consolas", 10)

class StarfieldLiteApp:
    def __init__(self, root):
        self.root = root
        self.root.title(WINDOW_TITLE)
        self.root.geometry("950x700")
        
        # 高 DPI 支持
        try:
            from ctypes import windll
            windll.shcore.SetProcessDpiAwareness(1)
        except:
            pass

        self.database = {}
        self.filtered_data = []
        
        # 【新增】代码反向索引:{ "00035A48": "战刀" }
        # 用于在导入时快速判断代码是否已存在
        self.code_index = {}

        # 智能获取运行目录
        if getattr(sys, 'frozen', False):
            self.base_dir = Path(sys.executable).parent.resolve()
        else:
            self.base_dir = Path(__file__).parent.resolve()
            
        self.db_path = self.base_dir / DB_FILE_NAME

        self.setup_styles()
        self.create_ui()
        self.load_database()

    def setup_styles(self):
        style = ttk.Style()
        if 'vista' in style.theme_names():
            style.theme_use('vista')
        elif 'clam' in style.theme_names():
            style.theme_use('clam')
            
        style.configure(".", font=(FONT_FAMILY, FONT_SIZE))
        style.configure("Treeview", rowheight=28, font=(FONT_FAMILY, 10))
        style.configure("Treeview.Heading", font=(FONT_FAMILY, 10, "bold"))
        style.configure("Status.TLabel", foreground="#555555", background="#f0f0f0")
        style.configure("Tool.TButton", padding=(10, 5))

    def create_ui(self):
        # === 顶部:搜索与操作 ===
        top_frame = ttk.Frame(self.root, padding="10")
        top_frame.pack(side=tk.TOP, fill=tk.X)
        
        search_frame = ttk.LabelFrame(top_frame, text=" 搜索 (名称/代码/分类)", padding=5)
        search_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        self.search_var = tk.StringVar()
        self.search_var.trace_add("write", lambda *args: self.perform_search())
        
        self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, font=(FONT_FAMILY, 12))
        self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        self.search_entry.focus_set()
        
        file_frame = ttk.Frame(top_frame)
        file_frame.pack(side=tk.RIGHT)
        
        ttk.Button(file_frame, text=" 手动加载库", command=self.manual_load_database).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_frame, text=" 智能导入 TXT", command=self.import_data_threaded).pack(side=tk.LEFT, padx=2)
        
        if EXCEL_SUPPORT:
            ttk.Button(file_frame, text=" 导出 Excel", command=self.export_to_excel).pack(side=tk.LEFT, padx=2)
        else:
            ttk.Button(file_frame, text="❌ 无 Excel", command=self.show_excel_error).pack(side=tk.LEFT, padx=2)
            
        ttk.Button(file_frame, text=" 导出备份", command=self.export_data).pack(side=tk.LEFT, padx=2)
        ttk.Button(file_frame, text="️ 清空", command=self.clear_data).pack(side=tk.LEFT, padx=2)

        # === 中部:列表展示区 ===
        list_frame = ttk.Frame(self.root, padding="10")
        list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
        
        columns = ("cat", "name", "code")
        self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", selectmode="browse")
        
        self.tree.heading("cat", text="分类", anchor="center")
        self.tree.heading("name", text="物品名称")
        self.tree.heading("code", text="代码 (ID)", anchor="center")
        
        self.tree.column("cat", width=90, minwidth=70, anchor="center")
        self.tree.column("name", width=350, minwidth=200)
        self.tree.column("code", width=150, minwidth=100, anchor="center")
        
        scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        self.tree.bind("<Double-1>", self.on_double_click)
        self.tree.bind("<Button-3>", self.on_right_click)

        # === 下部:代码工具箱 ===
        tool_frame = ttk.LabelFrame(self.root, text="️ 代码生成工具箱", padding=10)
        tool_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5)
        
        row1 = ttk.Frame(tool_frame)
        row1.pack(fill=tk.X, pady=(0, 5))
        
        ttk.Label(row1, text="原始代码 ID:").pack(side=tk.LEFT, padx=(0, 5))
        self.input_raw = ttk.Entry(row1, width=20, font=CODE_FONT)
        self.input_raw.pack(side=tk.LEFT, padx=(0, 10))
        
        ttk.Button(row1, text=" 物品", command=lambda: self.generate_code("player.additem")).pack(side=tk.LEFT, padx=2)
        ttk.Button(row1, text="⭐ 技能", command=lambda: self.generate_code("player.addperk")).pack(side=tk.LEFT, padx=2)
        ttk.Button(row1, text=" 属性", command=lambda: self.generate_code("player.setav")).pack(side=tk.LEFT, padx=2)
        
        row2 = ttk.Frame(tool_frame)
        row2.pack(fill=tk.X)
        
        ttk.Label(row2, text="生成命令:").pack(side=tk.LEFT, padx=(0, 5))
        self.input_result = ttk.Entry(row2, width=50, font=CODE_FONT, state="readonly")
        self.input_result.pack(side=tk.LEFT, padx=(0, 10), fill=tk.X, expand=True)
        
        self.btn_copy_tool = ttk.Button(row2, text=" 复制命令", command=self.copy_tool_result)
        self.btn_copy_tool.pack(side=tk.RIGHT)

        # === 底部:状态栏 ===
        self.status_var = tk.StringVar()
        self.status_var.set(f"就绪 | 版本 {VERSION}")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W, style="Status.TLabel")
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)

    # --- 核心逻辑 ---

    def load_database(self):
        if not self.db_path.exists():
            self.status_var.set(f"未找到数据库 | 版本 {VERSION} (请导入 TXT)")
            self.database = {}
            self.code_index = {}
            self.update_status()
            return
        
        try:
            with open(self.db_path, 'r', encoding='utf-8') as f:
                content = f.read().strip()
                self.database = json.loads(content) if content else {}
            
            # 【关键】加载时重建代码索引
            self.rebuild_code_index()
            
            self.perform_search()
            self.status_var.set(f"已加载:{len(self.database)}条数据 | 版本 {VERSION}")
        except Exception as e:
            messagebox.showerror("数据库错误", f"读取失败:\n{e}\n建议清空或重新导入。")
            self.database = {}
            self.code_index = {}
            self.update_status()

    def rebuild_code_index(self):
        """遍历数据库,重建 code -> name 映射"""
        self.code_index = {}
        for name, info in self.database.items():
            code = info.get('code', '').upper()
            if code:
                # 如果发现有多个名字对应同一个代码(脏数据),保留第一个遇到的
                if code not in self.code_index:
                    self.code_index[code] = name

    def manual_load_database(self):
        file_path = filedialog.askopenfilename(
            title="选择数据库文件 (JSON)",
            filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
            initialdir=self.base_dir
        )
        if not file_path: return
            
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                temp_data = json.load(f)
            
            count = len(temp_data)
            if not messagebox.askyesno("确认加载", f"找到 {count} 条数据。\n确定加载并设为默认?\n注意:这将覆盖当前内存中的所有数据。"):
                return

            shutil.copy(file_path, self.db_path)
            self.database = temp_data
            self.rebuild_code_index()
            self.perform_search()
            self.status_var.set(f"✅ 已加载 {count} 条 | 版本 {VERSION}")
            messagebox.showinfo("成功", "数据库加载成功!")
            
        except json.JSONDecodeError:
            messagebox.showerror("格式错误", "文件不是有效的 JSON。")
        except Exception as e:
            messagebox.showerror("加载失败", str(e))

    def save_database(self):
        try:
            with open(self.db_path, 'w', encoding='utf-8') as f:
                json.dump(self.database, f, ensure_ascii=False, indent=2)
            # 保存后不需要重建索引,因为只是修改现有数据,除非新增了条目
            # 但为了保险,如果在导入过程中调用了 save,索引已经在内存中维护好了
            self.update_status()
        except Exception as e:
            messagebox.showerror("保存错误", str(e))

    def normalize_category(self, cat_str):
        cat_str = cat_str.strip()
        aliases = {
            "weapon": "武器", "suit": "太空服", "armor": "太空服",
            "helmet": "头盔", "bag": "背包", "skill": "技能",
            "perk": "技能", "stat": "属性", "ammo": "弹药",
            "throwable": "投掷物", "note": "笔记", "resource": "资源",
            "misc": "杂项", "rescue": "救援", "med": "救援"
        }
        lower_cat = cat_str.lower()
        if lower_cat in aliases:
            return aliases[lower_cat]
        return cat_str if cat_str else "杂项"

    def perform_search(self):
        query = self.search_var.get().lower()
        self.filtered_data = []
        
        for name, info in self.database.items():
            cat = info.get('cat', '杂项')
            code = info['code']
            fav = info.get('fav', False)
            
            match = not query or (query in name.lower() or query in code.lower() or query in cat.lower())
            
            if match:
                self.filtered_data.append({"name": name, "code": code, "cat": cat, "fav": fav})
        
        self.filtered_data.sort(key=lambda x: (not x['fav'], x['cat'], x['name']))
        self.refresh_tree()
        self.update_status()

    def refresh_tree(self):
        self.tree.delete(*self.tree.get_children())
        for item in self.filtered_data:
            name = item['name']
            code = item['code']
            cat = item['cat']
            fav = item['fav']
            
            display_name = f"⭐ {name}" if fav else name
            display_cat = f"[{cat}]"
            self.tree.insert("", tk.END, values=(display_cat, display_name, code))

    def update_status(self):
        total = len(self.database)
        current = len(self.filtered_data)
        self.status_var.set(f"就绪 | 版本 {VERSION} | 总数:{total} | 结果:{current}")

    # --- 交互事件 ---

    def on_double_click(self, event):
        selection = self.tree.selection()
        if not selection: return
        code = self.tree.item(selection[0])['values'][2]
        self.copy_to_clipboard(str(code))
        self.status_var.set(f"✅ 已复制:{code}")

    def on_right_click(self, event):
        item_id = self.tree.identify_row(event.y)
        if not item_id: return
        
        self.tree.selection_set(item_id)
        item = self.tree.item(item_id)
        vals = item['values']
        
        real_cat = vals[0].strip("[]")
        name_raw = vals[1]
        code = vals[2]
        
        is_fav = name_raw.startswith("⭐ ")
        real_name = name_raw[2:] if is_fav else name_raw

        menu = tk.Menu(self.root, tearoff=0)
        menu.add_command(label=" 复制代码", command=lambda: self.copy_to_clipboard(code))
        menu.add_command(label="✏️ 修改代码", command=lambda: self.edit_item(real_name, 'code'))
        menu.add_command(label="️ 修改分类", command=lambda: self.edit_item(real_name, 'cat'))
        menu.add_separator()
        fav_text = "❌ 取消收藏" if is_fav else "⭐ 加入收藏"
        menu.add_command(label=fav_text, command=lambda: self.toggle_favorite(real_name))
        menu.add_separator()
        menu.add_command(label="️ 删除此项", command=lambda: self.delete_item(real_name), foreground="red")
        
        menu.post(event.x_root, event.y_root)

    def edit_item(self, name, field):
        if name not in self.database: return
        current_val = self.database[name].get(field, '杂项' if field == 'cat' else '')
        prompt = f"请输入新的{field}:" if field == 'code' else "请输入新的分类 (如:武器):"
        
        new_val = simpledialog.askstring(f"修改 {field}", prompt, initialvalue=current_val)
        if new_val and new_val.strip():
            old_code = self.database[name].get('code', '')
            
            if field == 'code':
                # 如果修改了代码,需要更新索引
                new_code = new_val.strip().upper()
                if new_code != old_code:
                    # 移除旧索引
                    if old_code in self.code_index and self.code_index[old_code] == name:
                        del self.code_index[old_code]
                    # 添加新索引(如果新代码没被占用)
                    if new_code in self.code_index:
                        messagebox.showwarning("代码冲突", f"代码 {new_code} 已存在于库中(属于 {self.code_index[new_code]})。\n已强制更新当前物品的代码。")
                    self.code_index[new_code] = name
            
            if field == 'cat':
                new_val = self.normalize_category(new_val)
               
            self.database[name][field] = new_val
            self.save_database()
            self.perform_search()

    def toggle_favorite(self, name):
        if name not in self.database: return
        self.database[name]['fav'] = not self.database[name].get('fav', False)
        self.save_database()
        self.perform_search()

    def delete_item(self, name):
        if messagebox.askyesno("确认删除", f"确定删除「{name}」?"):
            code = self.database[name].get('code', '')
            # 从索引中移除
            if code in self.code_index and self.code_index[code] == name:
                del self.code_index[code]
            
            del self.database[name]
            self.save_database()
            self.perform_search()

    def copy_to_clipboard(self, text):
        self.root.clipboard_clear()
        self.root.clipboard_append(text)
        self.root.update()

    def generate_code(self, prefix):
        raw = self.input_raw.get().strip()
        if not raw:
            messagebox.showwarning("提示", "请先输入代码 ID")
            return
        clean_raw = re.sub(r'^player\.\w+\s+', '', raw)
        result = f"{prefix} {clean_raw}"
        self.input_result.config(state="normal")
        self.input_result.delete(0, tk.END)
        self.input_result.insert(0, result)
        self.input_result.config(state="readonly")

    def copy_tool_result(self):
        val = self.input_result.get()
        if val:
            self.copy_to_clipboard(val)
            self.status_var.set(f"✅ 已复制:{val}")

    # --- 导入/导出逻辑 (核心修改部分) ---

    def import_data_threaded(self):
        file_path = filedialog.askopenfilename(title="选择数据文件 (TXT)", filetypes=[("Text Files", "*.txt")])
        if not file_path: return
        
        win = tk.Toplevel(self.root)
        win.title("正在智能导入...")
        win.geometry("300x100")
        win.transient(self.root)
        win.grab_set()
        
        ttk.Label(win, text="正在解析文件...\n检测 Sorting 标记").pack(pady=20)
        pb = ttk.Progressbar(win, mode='indeterminate')
        pb.pack(fill=tk.X, padx=20)
        pb.start(10)
        
        def worker():
            try:
                stats = self.process_import_file_smart(file_path)
                self.root.after(0, lambda: self.finish_import(win, stats))
            except Exception as e:
                self.root.after(0, lambda: messagebox.showerror("导入失败", str(e)))
                self.root.after(0, win.destroy)
        
        threading.Thread(target=worker, daemon=True).start()

    def process_import_file_smart(self, path):
        """
        智能导入逻辑:
        1. 检测 'sorting 分类名' 行来切换当前分类。
        2. 检测代码唯一性,如果代码已存在,跳过该行(防止重复)。
        """
        new_count = 0
        dup_code_count = 0
        dup_name_count = 0
        current_category = "杂项"
        categories_found = set()
        
        with open(path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
        
        if not lines:
            return {"new": 0, "dup_code": 0, "dup_name": 0, "cats": []}

        for line in lines:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            
            # 【策略 1】检测 Sorting 标记
            # 匹配 "sorting 武器" 或 "Sorting:头盔" 等格式
            sort_match = re.match(r'^sorting\s*[::]?\s*(.+)$', line, re.IGNORECASE)
            if sort_match:
                current_category = self.normalize_category(sort_match.group(1))
                categories_found.add(current_category)
                continue
            
            # 【策略 2】解析数据行
            # 支持 Tab 或多个空格分隔
            parts = re.split(r'\t+|\s{2,}', line)
            
            if len(parts) >= 2:
                code = parts[0].strip().upper() # 统一转大写方便比对
                name = parts[1].strip()
               
                if not code or not name:
                    continue

                # 【策略 3】代码唯一性检查
                if code in self.code_index:
                    # 代码已存在
                    existing_name = self.code_index[code]
                    if existing_name == name:
                        # 完全重复(名和码都一样)
                        dup_code_count += 1
                    else:
                        # 代码相同但名字不同(如:战刀 vs 战斗刀)
                        # 策略:保留旧数据,跳过新数据,计数为“代码重复”
                        dup_code_count += 1
                    continue
               
                # 【策略 4】名字重复检查(可选,防止同名不同码,但通常代码唯一更重要)
                # 这里我们主要依赖代码唯一性。如果名字重复但代码不同,视为不同物品(可能是翻译差异)
                # 如果希望名字也唯一,可以解开下面注释
                """
                if name in self.database:
                    dup_name_count += 1
                    continue
                """

                # 入库
                self.database[name] = {
                    "code": code,
                    "cat": current_category,
                    "fav": False
                }
                # 更新索引
                self.code_index[code] = name
                new_count += 1
        
        self.save_database()
        return {
            "new": new_count,
            "dup_code": dup_code_count,
            "dup_name": dup_name_count,
            "cats": sorted(list(categories_found))
        }

    def finish_import(self, win, stats):
        win.destroy()
        
        cat_list = ", ".join(stats['cats']) if stats['cats'] else "无新分类"
        msg = (f"✅ 智能导入完成!\n\n"
               f" 涉及分类:【{cat_list}】\n"
               f"➕ 新增物品:{stats['new']}\n"
               f"⚠️ 跳过代码重复:{stats['dup_code']} (代码已存在,保留旧数据)\n")
               # f"⚠️ 跳过名字重复:{stats['dup_name']}") # 如果启用了名字检查
        
        messagebox.showinfo("导入结果", msg)
        self.perform_search()
        self.status_var.set(f"导入成功 | 新增 {stats['new']} 条 | 版本 {VERSION}")

    def export_data(self):
        path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text Files", "*.txt")])
        if not path: return
        try:
            grouped = {}
            for n, i in self.database.items():
                c = i.get('cat', '杂项')
                grouped.setdefault(c, []).append((n, i))
            
            with open(path, 'w', encoding='utf-8') as f:
                f.write("# Starfield Database Backup\n")
                for c, items in sorted(grouped.items()):
                    f.write(f"sorting {c}\n") # 导出时也带上 sorting 标记,方便下次导入
                    for n, i in items:
                        f.write(f"{i['code']}\t{n}\n")
                    f.write("\n")
            messagebox.showinfo("成功", f"文本备份已导出:\n{path}")
        except Exception as e:
            messagebox.showerror("错误", str(e))

    def show_excel_error(self):
        messagebox.showwarning("缺少依赖",
            "未检测到 'openpyxl' 库。\n\n"
            "如需导出 Excel,请在命令行运行:\n"
            "pip install openpyxl\n"
            "然后重新打包程序。")

    def export_to_excel(self):
        if not EXCEL_SUPPORT:
            self.show_excel_error()
            return
            
        path = filedialog.asksaveasfilename(
            defaultextension=".xlsx",
            filetypes=[("Excel Files", "*.xlsx")],
            initialfile=f"Starfield_Data_{VERSION}.xlsx"
        )
        if not path: return
        
        try:
            wb = Workbook()
            ws = wb.active
            ws.title = "Items"
            
            headers = ["分类", "物品名称", "代码 ID", "收藏"]
            ws.append(headers)
            
            for name, info in self.database.items():
                cat = info.get('cat', '杂项')
                code = info['code']
                fav = "是" if info.get('fav', False) else "否"
                ws.append([cat, name, code, fav])
            
            for col in ws.columns:
                max_length = 0
                column = col[0].column_letter
                for cell in col:
                    if cell.value:
                        max_length = max(max_length, len(str(cell.value)))
                ws.column_dimensions[column].width = min(max_length + 2, 50)
            
            bold_font = Font(bold=True)
            for cell in ws[1]:
                cell.font = bold_font
               
            wb.save(path)
            messagebox.showinfo("成功", f"Excel 已导出:\n{path}\n共 {len(self.database)} 条数据。")
            
        except Exception as e:
            messagebox.showerror("导出失败", str(e))

    def clear_data(self):
        if messagebox.askyesno("高危警告", "确定要清空所有数据吗?\n此操作不可恢复!"):
            self.database = {}
            self.code_index = {}
            self.save_database()
            self.perform_search()
            messagebox.showinfo("已清空", "数据库已重置。")

if __name__ == "__main__":
    root = tk.Tk()
    app = StarfieldLiteApp(root)
    root.mainloop()
回复 支持 反对

使用道具 举报

15

主题

1034

帖子

7015

积分

游戏精英

Rank: 8Rank: 8

贡献度
89
金元
66585
积分
7015
精华
0
注册时间
2024-2-14

尼禄·克劳狄乌斯雷姆性情中人拉姆锦鲤

冰凉的地板
发表于 2026-3-8 19:15 | 只看该作者
666666666a ···
回复 支持 1 反对 0

使用道具 举报

2

主题

401

帖子

521

积分

高级玩家

Rank: 4

贡献度
6
金元
4974
积分
521
精华
0
注册时间
2015-6-1
5#
 楼主| 发表于 2026-3-8 19:18 | 只看该作者
回复 支持 反对

使用道具 举报

0

主题

772

帖子

1059

积分

游戏狂人

Rank: 6Rank: 6

贡献度
0
金元
10589
积分
1059
精华
0
注册时间
2006-11-13
6#
发表于 2026-3-10 16:16 | 只看该作者
谢谢分享
回复 支持 反对

使用道具 举报

0

主题

223

帖子

325

积分

高级玩家

Rank: 4

贡献度
0
金元
3245
积分
325
精华
0
注册时间
2018-10-1
7#
发表于 2026-3-15 18:32 | 只看该作者
66666666666666666666666666
回复 支持 反对

使用道具 举报

0

主题

16

帖子

33

积分

初级玩家

Rank: 2

贡献度
0
金元
332
积分
33
精华
0
注册时间
2024-6-24
8#
发表于 2026-3-16 23:26 | 只看该作者
65666666666666666666666666
回复 支持 反对

使用道具 举报

10

主题

1691

帖子

2112

积分

游戏达人

Rank: 7Rank: 7Rank: 7

贡献度
129
金元
15957
积分
2112
精华
0
注册时间
2006-11-17
9#
发表于 2026-3-18 23:02 | 只看该作者
这个很棒啊
回复 支持 反对

使用道具 举报

35

主题

1203

帖子

1287

积分

游戏狂人

Rank: 6Rank: 6

贡献度
0
金元
12868
积分
1287
精华
0
注册时间
2016-2-23
10#
发表于 2026-3-19 09:56 | 只看该作者
666666666666
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|3DMGAME ( 京ICP备14006952号-1  沪公网安备 31011202006753号

GMT+8, 2026-3-19 12:58 , Processed in 0.037390 second(s), 20 queries , Memcached On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表