暗黑破坏神2 MOD修改工具装备编辑其他物品

装备编辑-其他物品 对应的是 misc.txt 这张杂项物品数据表。在暗黑破坏神2 的 TXT 数据体系中,misc.txt 不是某一类单纯装备底材表,而是承载了大量"非武器、非防具"的基础物品配置。它既包含 namecodenamestr 这类基础标识字段,也包含 spawnableraritylevelcost 这类生成与经济字段,还包含 useablestackableminstackmaxstackquestmissiletypespelliconpspell 等与可使用、堆叠、任务、投射物和法术行为相关的字段。也就是说,这个模块影响的不是单一装备强度,而是杂项物品在游戏中的生成、使用、堆叠、显示和特殊行为。

从当前项目源码来看,装备编辑-其他物品 继续沿用装备编辑模块的共享架构。页面层只有一个很薄的 ItemEquipMiscTab.vue,它把 misc.txt 交给通用 ItemEquipEditor。真正的字段解析、分组展示、条目检索、复制、描述编辑和保存逻辑,都由公共控制器、公共分组配置和 Tauri 本地写回链路完成。和腰带编辑不同,misc.txt 被当前通用编辑器识别为基础装备文件,因此页面主操作按钮显示为"复制",适合在已有杂项物品条目上扩展新记录。

文章目录

文件说明

从模块结构看,装备编辑-其他物品 并不是独立开发的一张表单页,而是共享装备编辑体系中的一个文件实例。ItemEditTab.vue 负责在装备编辑页签中切换到 misc.txtItemEquipMiscTab.vue 负责把文件名传给 ItemEquipEditor,控制器负责把表头、当前行和列元数据转换成界面可消费字段,公共 grouping 配置负责把字段归入不同业务分组,最终由 Tauri 命令把整张 misc.txt 写回本地文件。

模块文件职责表如下。这里既列出当前主链路实际运行的文件,也保留 misc 目录下存在但当前主编辑链路未看到直接引用的标准化 grouping/layout 文件。

文件名 文件类型 模块职责 与界面或数据处理的关系 备注
src/modules/itemEdit/ItemEditTab.vue Vue 装备编辑总入口 顶部文件页签切换到 misc.txt 时挂载其他物品子页 当前模块页面入口
src/modules/itemEdit/misc/ItemEquipMiscTab.vue Vue 其他物品页包装组件 仅把 file-name="misc.txt" 传给通用编辑器 当前模块最薄的一层
src/modules/itemEdit/ItemEquipEditor.vue Vue 通用装备编辑器 提供搜索、条目切换、分组切换、复制、保存和描述编辑 当前模块主界面
src/pages/home/useHomePageController.js JS 装备编辑状态装配 生成 itemEquipFileTabscurrentItemEquipFieldsitemEquipGroupTabscurrentItemEquipActiveGroup 等运行时数据 状态与解析层核心
src/pages/home/itemEquipHelpers.js JS 装备数据读取与保存 负责 loadItemEquipFileupdateItemEquipCelladdItemEquipRowsaveItemEquipFile 和表头校验 当前模块前端数据层
src/pages/home/tableDataLoader.js JS 通用 TXT 读取缓存 py_load_table 结果做缓存、去重和失效控制 读取性能层
src/modules/itemEdit/common/itemEquip.grouping.json JSON 通用字段分组规则 按列名、关键词、说明和范围组把字段归入业务分组 当前其他物品页实际分组主配置
src/modules/itemEdit/common/itemEquipGrouping.js JS 通用分组解析器 根据列名、range_group、说明文本等解析字段分组 当前其他物品页实际分组解析层
src/modules/itemEdit/misc/misc.grouping.json JSON 其他物品模块标准化分组配置 定义"基础信息/其他"两组的包装配置 当前源码未看到直接引用
src/modules/itemEdit/misc/misc.layout.json JSON 其他物品模块布局顺序 定义 misc 包装配置的组顺序 当前源码未看到直接引用
src/modules/itemEdit/misc/miscGrouping.js JS 其他物品模块分组解析封装 通过 groupingFactory 输出解析器接口 当前源码未看到直接引用
src/assets/resources/index.json JSON 列元数据索引 misc.txt.columns.json 纳入统一资源索引 元数据入口
src/assets/resources/files/misc.txt.columns.json JSON 杂项物品字段中文与说明 提供列中文名、字段类型、说明和范围组信息 当前模块字段说明层
src-tauri/src/main.rs Rust Tauri 命令注册 暴露 py_load_tablepy_save_table_json 给前端调用 命令桥接层
src-tauri/src/commands/files.rs Rust 本地 TXT 读写命令 负责 misc.txt 的编码读取、表头解析、整表写回和备份生成 当前模块后端数据层

把这些文件按层级拆开后,当前模块的结构就很清楚了。ItemEditTab.vueItemEquipMiscTab.vue 属于页面壳层,负责把"其他物品"放进正确的装备编辑入口。ItemEquipEditor.vue 属于通用视图层,负责把当前行渲染成分组表单。useHomePageController.jsitemEquipHelpers.js 属于状态和数据层,负责字段转换、读取、复制、保存和表头校验。misc.txt.columns.json 与公共 grouping 文件属于配置与解析层。files.rs 则是真正的本地落盘层。

再看真正参与界面构建、字段展示、分组控制、复制行为和保存逻辑的方法与状态。

字段/配置项/方法 所属文件 作用 在界面中的体现 修改后的影响
itemEquipFileTabs src/pages/home/useHomePageController.js 定义装备编辑文件页签 顶部显示"武器物品、防具物品、腰带编辑、其他物品..." 决定当前进入哪个文件编辑器
itemEquipState.fileName useHomePageController.js / ItemEditTab.vue 当前编辑中的文件名 选中"其他物品"后值为 misc.txt 决定读取、分组和保存目标
loadItemEquipFile src/pages/home/itemEquipHelpers.js 读取当前装备文件 页面进入、点击刷新时加载 misc.txt 重建 header、rows、列映射和搜索缓存
currentItemEquipFields src/pages/home/useHomePageController.js 把表头、当前行和列元数据转成字段对象数组 右侧字段标签、tooltip 和当前值都来自这里 决定界面可消费的数据结构
resolveItemEquipGroupByField src/modules/itemEdit/common/itemEquipGrouping.js 给当前字段判定业务分组 页面显示"基础标识、需求与价格、掉落与生成"等分组 决定字段显示位置
itemEquipGroupTabs src/pages/home/useHomePageController.js 输出当前分组名列表 左侧组切换按钮 控制右侧当前显示分组
currentItemEquipActiveGroup src/pages/home/useHomePageController.js 当前选中的字段分组对象 当前分组字段被渲染成输入网格 决定当前编辑焦点
rowKeyword / rowFilterColumn ItemEquipEditor.vue / state 条目检索关键词与过滤列 页面顶部"数据检索"区域 缩小杂项物品条目搜索范围
isBaseItemFile ItemEquipEditor.vue 判断是否属于基础装备文件 misc.txt 被识别为基础装备文件 影响按钮文案和复制行为
addOrCopyActionLabel ItemEquipEditor.vue 区分"新增"与"复制" 其他物品页按钮文字显示为"复制" 决定当前主增量动作
handleAddOrCopyItemEquipRow ItemEquipEditor.vue 当前条目复制入口 点击"复制"按钮触发 复制当前杂项物品行并生成新键值
validateItemEquipHeaderByFile src/pages/home/itemEquipHelpers.js 保存前校验文件表头 界面无直接显示,保存时执行 misc.txt 必须具备 namecompactsavecodelevelreq 等签名列
saveItemEquipAndDescription ItemEquipEditor.vue 联合保存杂项物品表与描述 点击"保存"按钮触发 先保存 misc.txt,处于描述分组时再保存描述
saveItemEquipFile src/pages/home/itemEquipHelpers.js 保存当前装备文件 页面主保存动作的核心实现 py_save_table_json 写回本地文件
setCachedTableResult src/pages/home/tableDataLoader.js 保存后刷新前端缓存 界面无感,但避免旧数据回显 保持前端读取结果与最新文件一致

misc.txt 的字段跨度很大,因此很适合从数据字段角度额外整理一张表。下表只选取当前源码中能够从 misc.txt.columns.json 直接确认的代表性字段,不补写无法确认的额外逻辑。

字段名 中文含义 所属分组 前端展示方式 实际用途说明
name 名称 基础标识 文本输入 定义杂项物品条目的引用名称
code 代码 基础标识 文本输入 作为物品唯一引用代码,复制时会参与新编码生成
namestr 名称字符串 基础标识 文本输入 对应基础物品名称文本键
version 版本 基础标识 版本选择或文本输入 区分经典模式与资料片模式
compactsave 紧凑存档 基础标识 / 其他 布尔输入 控制存档是否只保存基础属性
spawnable 可生成 掉落与生成 布尔输入 控制该杂项物品是否参与随机生成
rarity 稀有度 掉落与生成 数字输入 控制随机生成概率
level 等级 需求与价格 数字输入 控制基础物品等级与掉落门槛
levelreq 等级需求 需求与价格 数字输入 控制角色使用该物品所需等级
cost 基础价格 需求与价格 数字输入 控制 NPC 售卖基础价格
gamble cost 赌博价格 需求与价格 数字输入 控制赌博界面金币价格
auto prefix 自动前缀 技能与词缀 文本输入 automagic.txt 的自动词缀组相关
useable 可使用 其他 布尔输入 控制物品是否可通过右键使用
stackable 可堆叠 其他 布尔输入 控制是否使用数量字段与堆叠逻辑
minstack 最小堆叠 其他 数字输入 控制允许的最小数量
maxstack 最大堆叠 其他 数字输入 控制允许的最大数量
spawnstack 生成堆叠 其他 数字输入 控制生成时的数量
type 类型 其他 文本输入 引用 ItemTypes.txt 中的物品类型
type2 次类型 其他 文本输入或数字输入 引用次级物品类型
belt 腰带可用 其他 文本输入 控制腰带物品使用的 belts.txt 索引
quest 任务 其他 文本输入 控制任务物品标记和相关任务功能
questdiffcheck 任务难度检测 其他 布尔输入 与任务物品在不同难度下的处理相关
missiletype 投射物类型 其他 文本输入 引用 Missiles.txt 中的投射物 ID
spellicon 法术图标 技能与词缀 数字输入 决定物品使用法术显示图标
pspell 使用法术 技能与词缀 文本输入 选择物品使用时的 spell function

从字段结构可以看出,其他物品 模块承担的是非常宽的杂项物品编辑职责。它不像武器模块主要围绕伤害,也不像防具模块主要围绕防御,而是把可使用、可堆叠、任务、投射物、音效、显示、生成和价格都集中在同一张表里。因此,这个模块单独作为装备编辑页签存在是合理的。

软件开发

从开发视角看,装备编辑-其他物品 是共享装备编辑器体系里一个字段跨度较大的模块。它仍然使用 ItemEquipEditor.vue 做 UI,使用 currentItemEquipFields 把当前行转换成字段对象,使用 itemEquip.grouping.jsonitemEquipGrouping.js 做字段归组,使用 itemEquipHelpers.js 做读取、校验和保存。与腰带模块不同的是,misc.txt 被当前逻辑识别为基础装备文件,因此复制按钮和复制链路都会启用。与武器、防具不同的是,misc.txt 在保存前存在明确表头签名校验,这说明项目作者对这张表的风险边界做了更强约束。

核心实现结构表如下。

实现层 核心文件/代码 作用说明 设计意义
页面入口层 ItemEditTab.vue / ItemEquipMiscTab.vue 把"其他物品"挂入装备编辑页,并把 misc.txt 传给通用编辑器 保持模块入口极简
字段解析层 currentItemEquipFields header + row + colMeta 转成界面字段对象 页面不直接处理原始二维数组
分组配置层 itemEquip.grouping.json / itemEquipGrouping.js 按列名、说明文本和范围组归类字段 用配置替代硬编码表单
视图渲染层 ItemEquipEditor.vue 提供搜索、条目选择、分组切换、字段编辑、复制、保存和描述编辑 不同装备文件共享统一交互
复制操作层 handleAddOrCopyItemEquipRow misc.txt 被识别为基础装备文件,因此走复制逻辑 适合基于已有杂项物品扩展新条目
表头校验层 validateItemEquipHeaderByFile misc.txt 保存前校验 namecompactsavecodelevelreq 降低误选错误文件后的保存风险
数据读取层 loadItemEquipFile / tableDataLoader.js 读取 misc.txt 并缓存结果 降低重复读取和状态抖动
保存链路层 saveItemEquipFile / py_save_table_json / save_table_json 恢复隐藏列、整表序列化、本地落盘、备份生成 保证保存完整性和回滚能力
描述联动层 saveItemEquipAndDescription / saveItemDescription 其他物品页可进入装备描述联合保存入口 把基础数据和文本键值放在同一保存动作中

页面入口代码

vue 复制代码
<template>
  <section class="item-edit-section">
    <div class="settings-tabs">
      <button
        v-for="file in itemEquipFileTabs"
        :key="`equip-file-${file.file}`"
        class="settings-tab"
        :class="{ active: itemEquipState.fileName === file.file }"
        @click="switchItemEquipFile(file.file)"
      >
        {{ file.label }}
      </button>
    </div>
    <section class="item-edit-card">
      <ItemEquipWeapons v-if="itemEquipState.fileName === 'weapons.txt'" />
      <ItemEquipArmor v-else-if="itemEquipState.fileName === 'armor.txt'" />
      <ItemEquipBelts v-else-if="itemEquipState.fileName === 'belts.txt'" />
      <ItemEquipMisc v-else-if="itemEquipState.fileName === 'misc.txt'" />
      <ItemEquipSets v-else-if="itemEquipState.fileName === 'Sets.txt'" />
      <ItemEquipSetItems v-else-if="itemEquipState.fileName === 'SetItems.txt'" />
      <ItemEquipUnique v-else />
    </section>
  </section>
</template>
vue 复制代码
<template>
  <ItemEquipEditor file-name="misc.txt" />
</template>

<script setup>
import ItemEquipEditor from '../ItemEquipEditor.vue'
</script>

这两段代码说明,其他物品页并没有独立页面壳层。ItemEditTab.vue 负责在文件页签里切换到 misc.txtItemEquipMiscTab.vue 只负责把文件名传入通用装备编辑器。当前模块的差异不在入口层,而在字段元数据、分组归类、复制逻辑和表头校验层。

字段解析与分组代码

js 复制代码
const currentItemEquipFields = computed(() => {
  const rowIndex = currentItemEquipRowIndex.value
  if (rowIndex < 0) return []

  const row = itemEquipState.rows[rowIndex] || []
  const hasMap = Object.keys(itemEquipState.colMeta || {}).length > 0
  const isCubeMain = normKey(itemEquipState.fileName) === 'cubemain.txt'

  return itemEquipState.header
    .map((col, index) => {
      const meta = itemEquipColMeta(col)
      const zh = (meta?.zh || '').trim()

      return {
        index,
        colName: col,
        en: col,
        zh,
        meta,
        tooltip: itemEquipColTooltip(col),
        value: row[index] || '',
      }
    })
    .filter((field) => {
      if (!hasMap) return true
      if (isCubeMain) {
        const raw = String(field?.colName || '').trim()
        if (raw.startsWith('*')) return false
        if (normKey(raw) === 'ladder') return false
        return true
      }
      return field.zh.length > 0
    })
})
js 复制代码
export function resolveItemEquipGroupByField(field) {
  const colName = String(field?.colName || '').trim()
  const meta = field?.meta || null
  const rangeGroup = normKey(meta?.range_group || '')
  const key = normKey(colName)
  const desc = normKey(`${meta?.desc_zh || ''} ${meta?.desc_en || ''} ${meta?.zh || ''}`)

  return resolveGroupByRule(key, rangeGroup, desc) || ITEM_EQUIP_GROUP_FALLBACK
}
json 复制代码
{
  "order": [
    "基础标识",
    "需求与价格",
    "攻防属性",
    "掉落与生成",
    "显示与背包",
    "镶嵌与孔",
    "技能与词缀",
    "词条属性",
    "其他"
  ],
  "fallback": "其他",
  "rules": [
    {
      "group": "基础标识",
      "columns": ["name", "index", "code", "namestr", "version", "*id", "id", "carry1"]
    },
    {
      "group": "需求与价格",
      "includes": ["req", "level", "cost"],
      "desc_keywords": ["需求", "价格"]
    },
    {
      "group": "掉落与生成",
      "includes": ["spawn", "rarity", "prob", "drop"],
      "desc_keywords": ["掉落", "生成"]
    },
    {
      "group": "显示与背包",
      "includes": ["inv", "gfx", "transform", "component"],
      "desc_keywords": ["显示", "背包"]
    },
    {
      "group": "技能与词缀",
      "prefixes": ["prop", "par", "min", "max"],
      "includes": ["skill", "spell"],
      "desc_keywords": ["技能", "词缀"]
    }
  ]
}

这一层代码说明,其他物品页的字段分组并不是在模板里写死的。控制器先把当前行转成统一字段对象,字段对象里包含列名、中文名、tooltip、当前值和元数据。分组解析器再根据列名、range_group、中文说明、英文说明和关键词判断字段属于哪个业务组。对于 misc.txt 来说,namecodenamestr 会进入"基础标识",levelreqcostgamble cost 会进入"需求与价格",spawnablerarity 会进入"掉落与生成",spelliconpspell 这类字段则更容易进入"技能与词缀"或 fallback 组。这样做的意义在于,即使 misc.txt 字段跨度很大,页面仍然能用相同的表单骨架完成展示。

从当前代码能够确认,misc 目录下也存在 miscGrouping.jsmisc.grouping.jsonmisc.layout.json 这一套标准化封装文件,但主编辑链路没有直接引用它们。当前运行时主要依赖公共 itemEquipGrouping。这说明其他物品模块已经具备独立配置扩展点,但实际展示仍由共享装备编辑体系统一管理。

复制行为与描述保存代码

js 复制代码
const isBaseItemFile = computed(() => {
  const file = normKey(itemEquipState.fileName)
  return file === 'weapons.txt' || file === 'armor.txt' || file === 'misc.txt'
})

const addOrCopyActionLabel = computed(() =>
  (isCopyModeFile.value || isBaseItemFile.value) ? '复制' : '新增',
)

async function handleAddOrCopyItemEquipRow() {
  if (isCopyModeFile.value || isBaseItemFile.value) {
    const rows = Array.isArray(itemEquipState.rows) ? itemEquipState.rows : []
    const header = Array.isArray(itemEquipState.header) ? itemEquipState.header : []
    if (!rows.length || !header.length) {
      addItemEquipRow()
      return
    }

    const current = getActiveRowIndex()
    const sourceIndex = current >= 0 && current < rows.length ? current : 0
    const sourceRow = Array.isArray(rows[sourceIndex]) ? rows[sourceIndex] : []
    const clone = [...sourceRow]
    while (clone.length < header.length) clone.push('')

    const keyColIndex = resolveCopyNameKeyColIndex(header)
    const codeColIndex = resolveCopyCodeColIndex(header)
    const sourceKey = keyColIndex >= 0 ? String(sourceRow?.[keyColIndex] || '').trim() : ''
    const targetKey = keyColIndex >= 0 ? buildCloneNameKey(sourceKey, rows, keyColIndex) : ''
    const sourceCode = codeColIndex >= 0 ? String(sourceRow?.[codeColIndex] || '').trim() : ''
    const targetCode = buildCloneCode(sourceCode, rows, codeColIndex)

    if (keyColIndex >= 0 && targetKey) clone[keyColIndex] = targetKey
    if (codeColIndex >= 0 && targetCode) clone[codeColIndex] = targetCode

    const insertAt = rows.length
    itemEquipState.rows.push(clone)
    itemEquipState.sourceRows.push([...clone])
    itemEquipState.activeRowIndex = insertAt
  }
}
js 复制代码
const descEnabledFiles = new Set(['weapons.txt', 'armor.txt', 'belts.txt', 'misc.txt', 'sets.txt', 'uniqueitems.txt'])
if (descEnabledFiles.has(normKey(itemEquipState.fileName)) && !list.some((item) => item.name === '装备描述')) {
  list.push({ name: '装备描述', fields: [] })
}

async function saveItemDescription() {
  const key = activeItemEditKey.value
  if (!key) return
  const zhCN = reverseMultilineForStorage(itemDescZhCN.value)
  const zhTW = reverseMultilineForStorage(itemDescZhTW.value)
  const enUS = resolveCurrentItemEnName()
  await saveItemNameEntryByKey(key, zhCN, zhTW, enUS)
}

async function saveItemEquipAndDescription() {
  const ok = await saveItemEquipFile()
  if (!ok) return
  if (!isDescriptionGroup.value) return
  try {
    await saveItemDescription()
  } catch (error) {
    appendLog?.(`同步保存描述失败: ${String(error)}`)
  }
}

misc.txt 在当前编辑器里被视为基础装备文件,因此主按钮显示为"复制"。这和腰带模块不同,腰带页走新增空白行,而其他物品页更适合从已有药剂、卷轴、钥匙、任务物品或其他杂项条目复制一份,再调整 namecodetype、堆叠、使用和生成相关字段。复制逻辑会尝试生成新的名称键和代码,并把复制后的行追加到表末尾。

从当前代码还能确认,misc.txt 被纳入"装备描述"分组的动态追加范围。也就是说,其他物品页不仅可以编辑基础 TXT 字段,还可以在描述分组中通过 saveItemNameEntryByKey 写回当前名称或描述键值。至于描述文本最终落入哪一份外部语言资源文件,当前片段无法完整确认,因此本文只写到当前源码明确暴露的保存入口。

读取、校验与保存代码

js 复制代码
function validateItemEquipHeaderByFile() {
  const file = normKey(itemEquipState.fileName)
  const header = Array.isArray(itemEquipState.header) ? itemEquipState.header : []
  const keys = new Set(header.map((h) => normKey(h)))

  const signatures = {
    'treasureclassex.txt': ['Treasure Class', 'Picks', 'Item1', 'Prob1'],
    'misc.txt': ['name', 'compactsave', 'code', 'levelreq'],
    'monstats.txt': ['Id', 'TreasureClass', 'TreasureClass(N)'],
    'monstats2.txt': ['Id', 'Height', 'OverlayHeight'],
  }

  // ...
}
js 复制代码
async function loadItemEquipFile(force = false) {
  if (!state.excelPath) {
    setItemEquipStatus('请先在文件配置定位 TXT 目录', 'error')
    return
  }

  try {
    itemEquipState.loading = true
    const file = itemEquipState.fileName || 'weapons.txt'
    const result = await loadTableData(state.excelPath, file, { force })

    itemEquipState.fileName = file
    itemEquipState.encoding = result.encoding || 'utf-8'
    itemEquipState.header = result.header || []
    itemEquipState.rows = (result.rows || []).map((row) => [...row])
    itemEquipState.sourceRows = cloneRows(result.rows || [])

    await loadItemEquipColumnMapping(file)
    await loadItemEquipSkillOptions()
    await loadTreasureClassNameOptions()
    rebuildItemEquipRowSearchCache()

    setItemEquipStatus(`已加载 ${file},共 ${itemEquipState.rows.length} 行`, 'ok')
    appendLog(`装备编辑已加载: ${file}`)
  } catch (error) {
    setItemEquipStatus(`加载失败: ${String(error)}`, 'error')
  } finally {
    itemEquipState.loading = false
  }
}
js 复制代码
async function saveItemEquipFile() {
  if (!state.excelPath || !itemEquipState.header.length) {
    setItemEquipStatus('请先加载道具文件', 'error')
    return false
  }

  try {
    const check = validateItemEquipHeaderByFile()
    if (!check.ok) {
      setItemEquipStatus(check.reason, 'error')
      appendLog(`装备编辑保存已拦截: ${check.reason}`)
      return false
    }

    itemEquipState.loading = true

    const hiddenColIndexes = itemEquipState.header
      .map((col, index) => ({ col, index }))
      .filter(({ col }) => {
        const meta = itemEquipColMeta(col)
        if (!meta) return false
        return String(meta?.zh || '').trim().length === 0
      })
      .map((item) => item.index)

    const sourceRows = cloneRows(itemEquipState.sourceRows)
    const rowsForSave = cloneRows(itemEquipState.rows).map((row, rowIndex) => {
      const source = sourceRows[rowIndex]
      if (!Array.isArray(source)) return row
      for (const colIndex of hiddenColIndexes) {
        row[colIndex] = String(source[colIndex] ?? '')
      }
      return row
    })

    const result = await invoke('py_save_table_json', {
      folder: state.excelPath,
      file: itemEquipState.fileName,
      encoding: itemEquipState.encoding,
      headerJson: JSON.stringify(itemEquipState.header),
      rowsJson: JSON.stringify(rowsForSave),
      backupPath: backupPath.value || buildBackupPath(state.modPath),
    })

    if (typeof syncSqliteTxtFile === 'function') {
      await syncSqliteTxtFile(itemEquipState.fileName, { logPrefix: '装备编辑保存后 SQLite' })
    }

    setItemEquipStatus(`保存成功: ${itemEquipState.fileName}`, 'ok')
    itemEquipState.rows = cloneRows(rowsForSave)
    itemEquipState.sourceRows = cloneRows(rowsForSave)
    setCachedTableResult(state.excelPath, itemEquipState.fileName, {
      encoding: itemEquipState.encoding,
      header: [...itemEquipState.header],
      rows: cloneRows(rowsForSave),
      row_count: itemEquipState.rows.length,
      path: joinExcelFilePath(state.excelPath, itemEquipState.fileName),
    })

    appendLog(`装备编辑保存成功: ${itemEquipState.fileName}`)
    return true
  } catch (error) {
    setItemEquipStatus(`保存失败: ${String(error)}`, 'error')
    return false
  } finally {
    itemEquipState.loading = false
  }
}
rust 复制代码
pub fn load_table(folder: String, file: String) -> Result<Value, String> {
    let path = table_path(&folder, &file);
    let (text, encoding) = read_text_with_encoding(&path)?;
    let (header, rows) = parse_tab_text(&text);
    if header.is_empty() {
        return Err(format!("header empty: {}", file.trim()));
    }
    let backup_dir = backup_dir()?;
    Ok(json!({
        "folder": PathBuf::from(folder.trim()).to_string_lossy(),
        "file": file.trim(),
        "path": path.to_string_lossy(),
        "encoding": encoding,
        "backup_dir": backup_dir.to_string_lossy(),
        "header": header,
        "rows": rows,
        "row_count": rows.len()
    }))
}

pub fn save_table_json(
    folder: String,
    file: String,
    encoding: String,
    header_json: String,
    rows_json: String,
    backup_path: Option<String>,
) -> Result<Value, String> {
    let path = table_path(&folder, &file);
    let header_values: Vec<Value> =
        serde_json::from_str(&header_json).map_err(|error| format!("invalid header: {error}"))?;
    let rows_values: Vec<Vec<Value>> =
        serde_json::from_str(&rows_json).map_err(|error| format!("invalid rows: {error}"))?;
    let header: Vec<String> = header_values
        .into_iter()
        .map(|v| v.to_string().trim_matches('\"').to_string())
        .collect();
    let rows: Vec<Vec<String>> = rows_values
        .into_iter()
        .map(|row| {
            row.into_iter()
                .map(|v| match v {
                    Value::String(s) => s,
                    other => other.to_string(),
                })
                .collect()
        })
        .collect();
    let backup_dir = normalize_backup_dir(backup_path.as_deref())?;
    let backup_path = write_table(&path, &header, &rows, &encoding, Some(&backup_dir))?;
    Ok(json!({
        "file": file.trim(),
        "path": path.to_string_lossy(),
        "encoding": encoding,
        "backup_path": backup_path,
        "backup_dir": backup_dir.to_string_lossy(),
        "row_count": rows.len()
    }))
}

这条链路说明了其他物品模块的数据闭环。读取时,前端通过 loadTableData 间接调用 py_load_table,拿到表头、行数据和编码信息,再加载 misc.txt.columns.json 对应的列元数据。保存时,saveItemEquipFile 会先执行 validateItemEquipHeaderByFile,其中 misc.txt 明确要求存在 namecompactsavecodelevelreq 这几个签名列。这个校验可以降低错误文件被当作杂项物品表保存的风险。写回前,代码还会把界面未展示的隐藏列从 sourceRows 恢复回来,避免保存时丢失原始数据。Tauri Rust 侧负责解析 JSON、恢复字符串二维表、按原编码写回,并生成备份路径。

从当前代码能够确认,完整后端保存链路是本地 Tauri 文件写回,不是远程 HTTP 接口。游戏运行中是否即时热加载 misc.txt,当前仓库没有看到对应监听或热更新实现,因此本文只围绕本地编辑和本地保存展开。

操作演示

从软件使用路径看,其他物品页的操作方式和武器、防具保持一致,但编辑内容更偏向杂项物品功能。进入装备编辑后切到"其他物品",页面会围绕 misc.txt 构建当前条目编辑环境。顶部区域负责检索和切换条目,左侧负责分组切换和保存动作,右侧负责编辑当前分组下的字段。

操作演示表如下。表中的行为都来自当前源码能够直接确认的页面逻辑和数据链路。

操作阶段 界面表现 操作动作 可确认结果
模块进入 装备编辑顶部显示多个文件页签 点击"其他物品" itemEquipState.fileName 切到 misc.txt,挂载 ItemEquipMiscTab
初次读取 页面进入后加载数据 自动读取或点击刷新 loadItemEquipFile 读取 misc.txt,生成 headerrows、列元数据和搜索缓存
条目检索 顶部有字段过滤下拉和关键字输入框 输入关键字或限制搜索列 name/code/index 或指定字段筛选杂项物品
条目切换 顶部条目选择器列出当前记录 点击某一条记录 activeRowIndex 切到目标行,右侧字段同步刷新
分组切换 左侧显示分组按钮 点击某个分组 currentItemEquipActiveGroup 切换到目标组,右侧只显示该组字段
编辑基础标识 基础标识组显示 namecodenamestr 等字段 修改标识字段 当前杂项物品的引用键值被更新
编辑生成经济 需求与价格、掉落与生成组显示 levellevelreqcostspawnablerarity 等字段 修改数值或开关 物品生成、等级门槛和价格相关参数被更新
编辑使用与堆叠 其他分组中显示 useablestackableminstackmaxstack 等字段 修改堆叠与使用参数 杂项物品的使用和数量逻辑被更新到当前行
编辑任务与投射物 其他分组中显示 questquestdiffcheckmissiletype 等字段 修改字段值 任务标记和投射物引用参数被更新
编辑法术显示 技能与词缀或其他分组中显示 spelliconpspell 等字段 修改法术相关字段 物品使用法术和图标相关参数被更新
复制当前条目 左侧动作按钮显示"复制" 点击"复制" 当前杂项物品行被复制,并尝试生成新的名称键和代码
描述编辑 分组中可出现"装备描述" 编辑文本并保存 当前名称或描述键值可通过联合保存入口写入
保存数据 点击"保存"按钮 保存当前修改 表头校验通过后,py_save_table_json 写回 misc.txt 并生成备份
缓存同步 保存后页面状态保持最新 再次读取或切换文件 setCachedTableResult 更新缓存,避免旧数据回显
游戏内验证 返回游戏环境查看杂项物品表现 外部验证 从当前代码能够确认文件已写回,运行时生效时机仍依赖具体环境

从操作角度看,其他物品页适合处理大量"行为型物品"。例如调整可堆叠数量、改变任务标记、修改使用法术、调整掉落与价格参数,都可以在同一页完成。复制当前条目的设计也很适合制作相似杂项物品,因为许多杂项物品之间往往共享相近结构,只需要改动少数字段即可形成新记录。

总结

装备编辑-其他物品 是这套暗黑破坏神2 MOD 修改工具里字段跨度最大的一类基础物品编辑模块。它并不聚焦伤害或防御,而是把生成、经济、堆叠、使用、任务、投射物、音效、显示和法术行为集中到一张 misc.txt 表里。当前工具没有把这些字段硬编码成专属页面,而是通过共享 ItemEquipEditor、公共字段分组、列元数据和本地保存链路,把复杂表结构整理成可检索、可分组、可复制、可保存的编辑界面。

从开发实现看,这个模块的价值在于复用和约束并存。一方面,它复用了装备编辑通用入口、字段解析、分组渲染、描述编辑和 Tauri 写回链路。另一方面,它又在 validateItemEquipHeaderByFile 中为 misc.txt 添加了明确表头签名,降低了误保存风险。对于长期维护 MOD 工具的开发者来说,这种设计比写死一张杂项物品大表更稳,也更适合后续继续扩展专属分组或专属控件。

相关推荐
Mr数据杨1 天前
暗黑破坏神2 MOD修改工具装备编辑暗金物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨1 天前
暗黑破坏神2 MOD修改工具装备编辑武器物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨1 天前
暗黑破坏神2 MOD修改工具游戏设置怪物等级
mod·游戏工具·暗黑破坏神2重置版
cqbzcsq2 个月前
MC Forge 1.20.1 mod开发学习笔记(战利品、标签、配方)
java·笔记·学习·mod·mc
cqbzcsq2 个月前
MC Forge1.20.1 mod开发学习笔记(个人向)
笔记·学习·mod·mc·forge
中二病码农不会遇见C++学姐3 个月前
文明六mod制作
mod
金山毒霸电脑医生3 个月前
植物大战僵尸融合版下载与安装教程:PC/安卓/iOS 全面指南
游戏·ios·植物大战僵尸·植物大战僵尸融合版·游戏工具
njsgcs4 个月前
sifumod经验3 水管臂问题
mod
长城20247 个月前
求余运算和数学模运算的知识了解
mod·取余·求余·求模·数学模运算·mod运算