暗黑破坏神2 MOD修改工具游戏设置整体难度

游戏设置-整体难度 是这套暗黑破坏神2 MOD 修改工具中非常典型的全局规则模块,对应的核心数据文件是 difficultylevels.txt。这张表并不负责单个怪物、单张地图或者单件装备的局部参数,而是统一定义普通、噩梦、地狱等难度下的一组全局平衡规则,包括抗性惩罚、死亡经验惩罚、吸血吸蓝换算、怪物强化系数、PVP 伤害倍率以及赌博系统概率参数。只要这张表发生修改,影响范围就会同时落到角色成长体验、战斗压力、掉落预期和玩法节奏上。

也正因为影响范围广,difficultylevels.txt 很适合被拆成独立编辑页,而不是混在通用 TXT 表格编辑器里直接逐列修改。当前项目源码采用的是"按难度行切换 + 按字段组展示"的界面结构,左侧负责切换 NormalNightmareHell 之类的难度行,右侧负责按"基础惩罚""控制与吸取""怪物强化""PVP倍率""赌博参数"等主题组织字段。这样的页面设计既符合工具使用场景,也更符合模块开发上的长期维护需求。

文章目录

文件说明

从源码结构看,游戏设置-整体难度 并不是单个 Vue 文件堆出的一页表单,而是由页面入口、子页签挂载、字段分组配置、列元数据映射、状态控制器、通用读取缓存、保存 helper 与 Tauri 文件命令共同拼装出来的。页面层只负责把难度行和分组字段展示出来,字段中文说明来自 columns.json,字段归组来自 grouping.jsonlayout.json,实际读取与保存则落到 settingsHelpers.js 和 Rust 的 files.rs 上。

模块文件职责表如下。这个表更适合从工程协作视角理解当前模块,因为它能直接看出哪些文件属于页面壳层,哪些属于配置层,哪些属于解析层,哪些属于数据读写层。

文件名 文件类型 模块职责 与界面或数据处理的关系 备注
src/pages/HomePage.vue Vue 主页面壳层 当主导航切到"游戏设置"时挂载 SettingsTab,并在页头显示当前加载状态 页面壳层
src/modules/settings/SettingsTab.vue Vue 游戏设置总入口 子页签切到 difficultyLevel 时渲染 SettingsDifficultyLevelTab 子模块挂载层
src/modules/settings/difficultyLevel/SettingsDifficultyLevelTab.vue Vue 整体难度编辑页 左侧切换难度行,右侧切换字段组并渲染编辑控件 当前模块核心界面
src/pages/home/useHomePageController.js JS 状态装配与计算 生成 difficultySettingRowsdifficultyGroupTabscurrentDifficultyActiveGroup 等页面所需数据结构 解析与状态层
src/pages/home/settingsHelpers.js JS 读取与保存 helper 负责 loadDifficultySettingsupdateDifficultySettingCellsaveDifficultySettings 数据交互层
src/pages/home/tableDataLoader.js JS 表读取缓存 py_load_table 结果做缓存、去重和失效控制 读取性能层
src/modules/settings/difficultyLevel/difficultylevels.grouping.json JSON 字段分组规则 通过 columns/includes/patterns 定义字段属于哪个分组 配置层
src/modules/settings/difficultyLevel/difficultylevels.layout.json JSON 分组顺序配置 控制"基础惩罚、控制与吸取、怪物强化、PVP倍率、赌博参数"的展示顺序 布局层
src/modules/settings/difficultyLevel/difficultylevelsGrouping.js JS 分组解析器 把 JSON 分组规则转换成运行时可调用的 resolveDifficultyGroupByField 解析层
src/assets/resources/index.json JSON 列元数据索引 difficultylevels.txt.columns.json 提供索引入口,供控制器动态加载 元数据入口
src/assets/resources/files/difficultylevels.txt.columns.json JSON 字段中文与说明元数据 提供字段中文标签、类型、说明,用于标签和 tooltip 展示 字段说明层
src-tauri/src/main.rs Rust Tauri 命令注册 暴露 py_load_tablepy_save_table_json 给前端调用 前后端桥接层
src-tauri/src/commands/files.rs Rust TXT 读写命令 负责把 difficultylevels.txt 解析成 header + rows,以及保存时回写并生成备份 数据层

这组文件之间的关系很明确。HomePage.vueSettingsTab.vue 解决的是"模块放在哪"这个问题,SettingsDifficultyLevelTab.vue 解决的是"页面长什么样"这个问题,useHomePageController.jsdifficultylevelsGrouping.js 解决的是"字段怎样组织给界面消费"这个问题,settingsHelpers.jsfiles.rs 解决的是"数据怎样读进来、怎样保存回去"这个问题。当前模块没有单独的后端接口服务,也没有额外的 Python 业务脚本参与运行时保存链路,能直接确认的主流程就是 Vue + JS + Tauri Rust 这一条本地桌面应用链路。

再看真正参与界面构建、字段展示、布局控制、数据更新和保存逻辑的字段、配置项与方法。这里不把所有变量机械展开,只保留当前模块主链路中真正发挥作用的部分。

字段/配置项/方法 所属文件 作用 在界面中的体现 修改后的影响
difficultySettingState.file src/pages/home/useHomePageController.js 定义数据源文件 页面始终围绕 difficultylevels.txt 工作 决定读取与保存目标
difficultySettingRows src/pages/home/useHomePageController.js 生成难度行列表 左侧垂直按钮区显示 Normal/Nightmare/Hell 影响当前编辑行
difficultySettingState.activeName SettingsDifficultyLevelTab.vue / controller 记录当前选中的难度名称 左侧按钮高亮与右侧字段内容联动 决定当前编辑的是哪一行
currentDifficultyRowIndex src/pages/home/useHomePageController.js 计算当前难度行下标 页面不直接显示,但所有字段都依赖该下标取值 决定数据读写行位置
currentDifficultyRowFields src/pages/home/useHomePageController.js header + row + colMeta 转成字段对象数组 右侧字段标签、tooltip、只读状态、当前值都由它提供 决定界面可消费的数据结构
resolveDifficultyGroupByField src/modules/settings/difficultyLevel/difficultylevelsGrouping.js 根据列名规则给字段归组 顶部分组按钮和字段归类都依赖它 修改规则会改变字段所属分组
difficultyGroupTabs src/pages/home/useHomePageController.js 输出当前可见分组名称 右侧顶部的分组切换按钮 决定界面展示顺序
currentDifficultyActiveGroup src/pages/home/useHomePageController.js 输出当前选中分组的字段集合 页面网格区只渲染这个分组内字段 决定当前画面可编辑字段
difficultySettingColTooltip src/pages/home/settingsHelpers.js 组合中文名、中文说明和英文说明 悬停标签时显示字段说明 提升字段可读性
updateDifficultySettingCell src/pages/home/settingsHelpers.js 把输入值写入内存中的 rows 输入即生效到页面状态 决定保存时写回的值
loadDifficultySettings src/pages/home/settingsHelpers.js 读取文件并初始化状态 刷新按钮与进入页面时触发 重建 header、rows、行索引与元数据
saveDifficultySettings src/pages/home/settingsHelpers.js 保存当前内存数据 点击保存按钮触发 调用 py_save_table_json 落盘并备份
setCachedTableResult src/pages/home/settingsHelpers.js 保存后更新前端缓存 页面无感,但避免旧数据回显 保持界面与文件内容一致
py_load_table / py_save_table_json src-tauri/src/main.rs Tauri 命令入口 界面无直接按钮,但所有读写通过它们完成 构成模块数据链路

整体难度模块从数据字段角度也很值得单独说明,因为它不像怪物等级工具那样只围绕一个"最大等级"参数运作,而是实打实地编辑整张全局规则表。下表挑选出真正具有代表性的字段,既能反映页面分组思路,也能反映这个模块在平衡设计中的实际价值。

字段名 中文含义 所属分组 前端展示方式 实际用途说明
Name 难度名称 行标识 在当前行中只读展示,不参与数值输入 用来区分 NormalNightmareHell
ResistPenalty 抗性惩罚(资料片) 基础惩罚 数字输入 影响高难度下角色抗性起点
ResistPenaltyNonExpansion 抗性惩罚(经典) 基础惩罚 数字输入 影响经典模式下抗性惩罚
DeathExpPenalty 死亡经验惩罚 基础惩罚 数字输入 影响死亡后经验损失比例
StaticFieldMin 静电下限 基础惩罚 数字输入 限制静电力场压低怪物生命的下限
LifeStealDivisor 生命偷取除数 控制与吸取 数字输入 影响吸血在不同难度下的换算结果
ManaStealDivisor 法力偷取除数 控制与吸取 数字输入 影响吸蓝在不同难度下的换算结果
MonsterSkillBonus 怪物技能加成 控制与吸取 数字输入 影响怪物技能强度的全局加成
UniqueDamageBonus 唯一怪伤害加成 怪物强化 数字输入 提高金怪威胁度
ChampionDamageBonus 精英怪伤害加成 怪物强化 数字输入 提高蓝精英威胁度
PlayerDamagePercentVSPlayer 玩家对玩家伤害系数 PVP倍率 数字输入 调整 PVP 输出平衡
MercenaryDamagePercentVSBoss 佣兵对 Boss 伤害系数 PVP倍率 数字输入 调整佣兵对首领输出效率
GambleRare 赌博稀有阈值 赌博参数 数字输入 调整赌博出稀有装备概率
GambleUnique 赌博唯一阈值 赌博参数 数字输入 调整赌博出唯一装备概率
GambleUltra 赌博精英升级阈值 赌博参数 数字输入 调整赌博升级到精英品质的概率参数

从这些字段就能看出,整体难度页不是单纯的"难度倍率页",它实际承担的是全局玩法平衡控制台的角色。抗性惩罚决定角色承压,偷取除数决定续航,怪物强化决定战斗压力,PVP 倍率决定角色与佣兵之间的伤害关系,赌博参数又把装备产出的一部分概率控制拉进了同一张表。也就是说,把这张表做成独立页面,不只是界面整理问题,更是系统分层问题。

软件开发

从开发实现角度看,游戏设置-整体难度 采用的是非常典型的配置驱动方案。页面组件不直接写死所有字段,也不在模板中硬编码每一列出现在哪个位置,而是先把 difficultylevels.txt 读成原始表格结构,再结合列元数据把每一列转成带中文名、tooltip、当前值、只读状态的字段对象,接着再通过 difficultylevels.grouping.jsondifficultylevels.layout.json 决定分组归属和展示顺序。这样做的核心好处在于,字段增加、字段改名、分组调整都可以在配置层和控制器层完成,不需要把页面模板改成难以维护的大体量表单。

核心实现结构表如下。它能比较清楚地反映当前模块为什么适合做配置驱动,而不是手工表单。

实现层 核心文件/代码 作用说明 设计意义
页面壳层 HomePage.vue / SettingsTab.vue 主导航进入"游戏设置",子页签进入"整体难度" 保持工具导航结构清晰
行选择层 useHomePageController.jsdifficultySettingRows 从表数据中提取难度行,并按 Normal/Nightmare/Hell 排序 用一张表支持多行切换,而不是多页面复制
字段解析层 currentDifficultyRowFields 把原始 header、row、colMeta 转成字段对象 让 UI 只消费统一字段结构
分组配置层 difficultylevels.grouping.json / difficultylevels.layout.json / difficultylevelsGrouping.js 按列名规则把字段归入固定业务组 降低页面硬编码程度
控件复用层 SettingsDifficultyLevelTab.vue + 通用组件 通过 NumberStepperVersionSelectEnabledSelectClassSelect 复用输入控件 让设置类模块共享同一套交互模式
读取保存层 settingsHelpers.js 负责读取、写值、保存、缓存同步、编辑器联动 页面组件不直接接触文件细节
后端数据层 files.rs 负责 TXT 编码读取、JSON 反序列化、写回与备份 把真实落盘逻辑留在 Rust 侧

页面入口代码

vue 复制代码
const settingsTabComponents = {
  charLevel: SettingsCharLevelTab,
  charStats: SettingsCharStatsTab,
  skillLevel: SettingsSkillLevelTab,
  monLevel: SettingsMonLevelTab,
  level: SettingsMonLevelTab,
  difficultyLevel: SettingsDifficultyLevelTab,
  hireling: SettingsHirelingTab,
}

const activeSettingsTabComponent = computed(() => settingsTabComponents[settingsTab.value] || SettingsCharLevelTab)

function switchSettingsTab(nextTab) {
  settingsTab.value = nextTab
  if (nextTab === 'skillLevel') {
    levelToolTab.value = 'skill'
    return
  }
  if (nextTab === 'monLevel' || nextTab === 'level') {
    levelToolTab.value = 'mon'
  }
}
js 复制代码
watch(settingsTab, async (value) => {
  if (activeTab.value !== 'settings') return

  if (value === 'difficultyLevel') {
    await loadDifficultySettings()
    return
  }
})

这一段实现说明,整体难度模块的入口并不复杂。SettingsTab.vue 负责把 difficultyLevel 映射到 SettingsDifficultyLevelTabuseHomePageController.js 负责在真正切到这个子页签时调用 loadDifficultySettings()。页面壳层只处理模块挂载,数据初始化则交给控制器监听完成,这样既避免了页面组件里混入太多初始化逻辑,也让"切页签就加载对应表"的行为保持统一。

难度行与字段分组计算代码

js 复制代码
const DIFFICULTY_NAME_ZH_MAP = {
  normal: '普通',
  nightmare: '噩梦',
  hell: '地狱',
}

function normalizeDifficultyName(value) {
  return String(value || '')
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '')
}

function difficultyOrder(value) {
  const key = normalizeDifficultyName(value)
  if (key === 'normal') return 0
  if (key === 'nightmare') return 1
  if (key === 'hell') return 2
  return 100
}

const difficultySettingRows = computed(() => {
  const idx = difficultySettingState.nameColIndex
  return difficultySettingState.rows
    .map((row, rowIndex) => {
      const name = String(row?.[idx] || '').trim()
      return {
        rowIndex,
        name,
        order: difficultyOrder(name),
        label: difficultyLabel(name),
      }
    })
    .filter((item) => item.name.length > 0)
    .sort((a, b) => {
      if (a.order !== b.order) return a.order - b.order
      return a.rowIndex - b.rowIndex
    })
})

const currentDifficultyRowFields = computed(() => {
  if (currentDifficultyRowIndex.value < 0) return []

  const row = difficultySettingState.rows[currentDifficultyRowIndex.value] || []
  const hasMap = Object.keys(difficultySettingState.colMeta || {}).length > 0

  return difficultySettingState.header
    .map((col, index) => {
      const meta = difficultySettingColMeta(col)
      return {
        index,
        colName: col,
        en: col,
        zh: (meta?.zh || '').trim(),
        meta,
        tooltip: difficultySettingColTooltip(col),
        value: row[index] || '',
        readonly: index === difficultySettingState.nameColIndex,
      }
    })
    .filter((field) => {
      if (!hasMap) return true
      return field.zh.length > 0
    })
})

const currentDifficultyFieldGroups = computed(() => {
  const order = DIFFICULTY_GROUP_ORDER
  const groups = new Map()

  for (const field of currentDifficultyRowFields.value) {
    const groupName = resolveDifficultyGroupByField(field)
    if (!String(groupName || '').trim()) continue
    if (!groups.has(groupName)) groups.set(groupName, [])
    groups.get(groupName).push(field)
  }

  return [...groups.entries()]
    .sort((a, b) => {
      const ai = order.indexOf(a[0])
      const bi = order.indexOf(b[0])
      const av = ai >= 0 ? ai : 999
      const bv = bi >= 0 ? bi : 999
      return av - bv
    })
    .map(([name, fields]) => ({ name, fields }))
})

这段控制器代码就是当前模块真正的"解析器"。当前项目并没有单独写一个 parser.ts 去转换 difficultylevels.txt,而是在 useHomePageController.js 里通过多个 computed 完成了解析职责。difficultySettingRows 负责把整张表的行变成左侧难度按钮,currentDifficultyRowFields 负责把当前行转成字段对象,currentDifficultyFieldGroups 再把这些字段塞进配置定义的分组里。页面组件消费到的已经不是原始二维数组,而是可直接渲染的结构化字段数据。

难度编辑界面代码

vue 复制代码
<template>
  <section class="charstats-section">
    <div class="settings-split-layout charstats-body">
      <aside class="settings-split-side charstats-side">
        <div class="charstats-side-list">
          <button
            v-for="item in difficultySettingRows"
            :key="`difficulty-${item.rowIndex}`"
            class="charstats-vtab"
            :class="{ active: difficultySettingState.activeName === item.name }"
            @click="difficultySettingState.activeName = item.name"
          >
            {{ item.label }}
          </button>
        </div>
        <div class="settings-split-side-actions charstats-side-actions">
          <button class="warning" :disabled="difficultySettingState.loading" @click="loadDifficultySettings({ force: true })">刷新</button>
          <button class="success" :disabled="difficultySettingState.loading || !difficultySettingState.header.length" @click="saveDifficultySettings">保存</button>
        </div>
      </aside>

      <section class="settings-split-main charstats-main">
        <div v-if="difficultyGroupTabs.length" class="charstats-group-tabs">
          <button
            v-for="groupName in difficultyGroupTabs"
            :key="`difficulty-group-${groupName}`"
            class="settings-tab"
            :class="{ active: difficultySettingState.activeGroup === groupName }"
            @click="difficultySettingState.activeGroup = groupName"
          >
            {{ groupName }}
          </button>
        </div>

        <div v-if="currentDifficultyActiveGroup?.fields?.length" class="exp-grid-wrap">
          <div class="exp-grid">
            <div v-for="field in currentDifficultyActiveGroup.fields" :key="`difficulty-field-${field.index}`" class="exp-field">
              <label :title="field.tooltip" class="charstats-field-label">
                <span class="field-zh">{{ field.zh || field.en }}</span>
              </label>

              <NumberStepper
                v-else
                :model-value="field.value"
                type="number"
                step="1"
                len-mode="layout"
                :readonly="field.readonly"
                :disabled="field.readonly"
                @update:model-value="updateDifficultyNumericField(field, $event)"
                @step="stepDifficultyNumericField(field, $event)"
              />
            </div>
          </div>
        </div>
      </section>
    </div>
  </section>
</template>

页面模板本身非常克制。左侧是难度行切换和刷新保存动作,右侧是分组按钮和字段网格。源码里虽然保留了 VersionSelectEnabledSelectClassSelect 这类通用分支,但从当前 difficultylevels.txt 字段集合能够确认,实际主力控件仍然是 NumberStepper。这说明当前模块并不是为某张表临时写死的一页,而是共享了一套设置页通用渲染框架,再让当前表按自身字段特点自然落到数值输入为主的表现上。

分组配置代码

json 复制代码
{
  "groups": {
    "基础惩罚": {
      "columns": [
        "resistpenalty",
        "resistpenaltynonexpansion",
        "deathexppenalty",
        "staticfieldmin"
      ]
    },
    "控制与吸取": {
      "columns": [
        "monsterskillbonus",
        "monsterfreezedivisor",
        "monstercolddivisor",
        "aicursedivisor",
        "lifestealdivisor",
        "manastealdivisor",
        "playerhitreactbuffervsmonster",
        "mercenarymaxstunlength"
      ]
    },
    "怪物强化": {
      "columns": [
        "uniquedamagebonus",
        "championdamagebonus",
        "monstercedamagepercent",
        "monsterfireenchantexplosiondamagepercent"
      ]
    },
    "PVP倍率": {
      "columns": [
        "playerdamagepercentvsplayer",
        "playerdamagepercentvsmercenary",
        "playerdamagepercentvsprimeevil",
        "mercenarydamagepercentvsplayer",
        "mercenarydamagepercentvsmercenary",
        "mercenarydamagepercentvsboss",
        "primeevildamagepercentvsplayer",
        "primeevildamagepercentvsmercenary",
        "primeevildamagepercentvspet",
        "petdamagepercentvsplayer"
      ]
    },
    "赌博参数": {
      "columns": [
        "gamblerare",
        "gambleset",
        "gambleunique",
        "gambleuber",
        "gambleultra",
        "([itemlevel]"
      ],
      "includes": [
        "gamble",
        "itemlevel"
      ]
    }
  }
}
js 复制代码
export function resolveDifficultyGroupByField(field) {
  const colName = String(field?.colName || field?.label || field?.en || '').trim()
  const key = normKey(colName)

  for (const rule of RULES) {
    if (rule.columns.includes(key)) return rule.group
    if (rule.patterns.some((regex) => regex.test(key))) return rule.group
    if (rule.includes.some((item) => item && key.includes(item))) return rule.group
  }

  return DIFFICULTY_GROUP_FALLBACK || ''
}

这一层配置直接体现了当前模块为什么不适合把表单写死在页面里。difficultylevels.txt 列数不少,字段语义跨度也很大,若全部硬写进模板,页面会迅速变成大体量条件分支。当前实现把归组责任交给 JSON 配置和 resolveDifficultyGroupByField,字段是依据列名规则动态落组的,展示顺序又由 difficultylevels.layout.json 统一控制。字段扩展时,只要列名符合既有规则,页面大多情况下不需要新增模板分支。

读取与保存代码

js 复制代码
async function loadDifficultySettings(options = {}) {
  const force = options.force === true

  if (!state.excelPath) {
    const message = '请先在文件配置定位 TXT 目录'
    setDifficultySettingStatus(message, 'error')
    appendLog(`整体难度读取失败: ${message}`)
    return
  }

  try {
    difficultySettingState.loading = true

    const result = await loadTableData(state.excelPath, difficultySettingState.file, { force })
    difficultySettingState.encoding = result.encoding || 'utf-8'
    difficultySettingState.header = result.header || []
    difficultySettingState.rows = (result.rows || []).map((row) => [...row])

    const idx = difficultySettingState.header.findIndex((item, i) => i === 0 || normKey(item) === 'name')
    difficultySettingState.nameColIndex = idx >= 0 ? idx : 0

    await loadDifficultyColumnMapping()

    const normalRow = difficultySettingRows.value.find((item) => normalizeDifficultyName(item.name) === 'normal')
    const first = normalRow || difficultySettingRows.value[0]
    if (!difficultySettingRows.value.some((item) => item.name === String(difficultySettingState.activeName || '').trim())) {
      difficultySettingState.activeName = first ? first.name : ''
    }

    setDifficultySettingStatus(`已加载 ${difficultySettingState.file},共 ${difficultySettingState.rows.length} 行`, 'ok')
    appendLog(`整体难度已加载: ${difficultySettingState.file}`)
  } catch (error) {
    setDifficultySettingStatus(`读取失败: ${String(error)}`, 'error')
    appendLog(`整体难度读取失败: ${String(error)}`)
  } finally {
    difficultySettingState.loading = false
  }
}

function updateDifficultySettingCell(rowIndex, colIndex, value) {
  if (rowIndex < 0 || colIndex < 0) return
  if (!difficultySettingState.rows[rowIndex]) return
  difficultySettingState.rows[rowIndex][colIndex] = String(value || '')
}

async function saveDifficultySettings() {
  if (!state.excelPath || !difficultySettingState.header.length) {
    setDifficultySettingStatus('请先读取 difficultylevels.txt', 'error')
    return
  }

  try {
    difficultySettingState.loading = true

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

    setDifficultySettingStatus('保存成功', 'ok')

    setCachedTableResult(state.excelPath, difficultySettingState.file, {
      encoding: difficultySettingState.encoding,
      header: [...difficultySettingState.header],
      rows: cloneRows(difficultySettingState.rows),
      row_count: difficultySettingState.rows.length,
      path: joinExcelFilePath(state.excelPath, difficultySettingState.file),
    })

    if (normKey(editorState.fileName) === normKey(difficultySettingState.file)) {
      await openEditorFile()
    }
  } catch (error) {
    setDifficultySettingStatus(`保存失败: ${String(error)}`, 'error')
  } finally {
    difficultySettingState.loading = false
  }
}

这一段 helper 代码把整体难度模块的读写链路完整串了起来。读取时,控制器会先通过 loadTableData 取回 headerrows,再找到 Name 列的位置,把当前难度行、当前字段、分组标签依次算出来。保存时,并不是逐个字段发请求,而是把当前整个 headerrows 序列化后交给 py_save_table_json。这意味着整体难度页本质上仍然是整表编辑器,只不过它在界面上做了"按行收缩 + 按组收缩"的视图组织。

Tauri 文件写回代码

rust 复制代码
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()
    }))
}

从当前代码能够确认,整体难度模块的后端保存链路是非常完整的本地 Tauri 写回流程。前端把整表序列化为 JSON,Rust 侧反序列化后恢复为字符串二维表,再按原编码写回 TXT,并返回备份路径与行数信息。也就是说,当前模块的保存行为不是模拟保存,也不是仅修改前端状态,而是真正落到了文件系统。仓库中没有看到独立 HTTP 服务、远程 API 或运行时热更新模块,因此能直接确认的后端实现主要集中在本地文件读写这一层。

从当前代码能够确认的内容主要集中在前端字段组织、本地 Tauri 读写链路、缓存同步与编辑器联动机制。游戏进程是否在运行时即时读取最新 difficultylevels.txt,仓库中暂时没有直接证据,所以本文只围绕可确认的文件读取、页面展示与本地落盘展开,不补写运行时热重载逻辑。

操作演示

就软件使用路径来说,整体难度模块的交互设计很适合频繁调参。进入页面后,并不会一次性看到整张 difficultylevels.txt 的全部列,而是先选中某个难度行,再切换某个字段分组,随后只在当前分组中调整数值。这样一来,修改噩梦难度的抗性惩罚时,不会同时被赌博参数和 PVP 参数干扰;调整地狱难度的怪物强化时,也不需要在整张表里横向搜索字段位置。

操作演示表如下。表中的行为全部来自当前源码能够确认的页面逻辑与保存链路。

操作阶段 界面表现 操作动作 数据变化或目的
模块进入 主导航进入"游戏设置",子页签点击"整体难度" 切换到 difficultyLevel 挂载 SettingsDifficultyLevelTab,并触发 loadDifficultySettings()
读取数据 页面初次加载或点击刷新按钮 触发读取 py_load_table 读取 difficultylevels.txt,重建 headerrows、行索引和列元数据
切换难度行 左侧显示 Normal/Nightmare/Hell 等行按钮 点击某个难度名称 更新 activeName,右侧字段值切换到对应行
切换字段组 右侧顶部显示"基础惩罚、控制与吸取、怪物强化、PVP倍率、赌博参数" 点击某个分组按钮 更新 activeGroup,网格区只展示该组字段
修改参数 字段标签下方显示数字输入控件 调整数值 updateDifficultySettingCell 直接改写内存中的 rows[rowIndex][colIndex]
保存数据 右下动作区点击保存 触发保存 py_save_table_json 把当前整表写回 difficultylevels.txt 并生成备份
校验保存结果 保存成功后查看顶部状态或再次刷新 重新读取 前端缓存更新,必要时文件编辑器同步刷新同名文件
游戏内效果验证 回到游戏环境验证难度差异 外部验证 从当前代码可确认文件已写回,具体生效时机仍依赖运行环境

从实际使用意义看,这种操作流很适合版本平衡迭代。举例来说,若需要只压低地狱难度的吸血收益,可以直接切到 Hell 行,再切到"控制与吸取"分组,调整 LifeStealDivisorManaStealDivisor,保存后再回到游戏验证。若要控制噩梦与地狱的死亡惩罚差异,则切到"基础惩罚"分组编辑 DeathExpPenalty 即可。字段分组和难度行切换的组合,让 difficultylevels.txt 这种全局规则表变成了可读、可控、可回滚的参数面板。

总结

游戏设置-整体难度 这个模块很能代表暗黑破坏神2 MOD 修改工具处理全局规则表时的工程思路。页面层尽量轻,状态层负责把整表数据转换成可编辑字段,配置层负责字段归组和布局顺序,数据层负责编码读取、文件写回和备份。这样带来的直接收益不是"代码更优雅"这么简单,而是工具在面对复杂 TXT 数据时更稳,字段增加时更容易扩展,保存风险也更容易控制。

对于工具使用场景来说,这个模块的价值在于把 difficultylevels.txt 从原始表格改造成了"按难度行 + 按业务分组"的编辑页。对于开发实现来说,它又展示了一种非常清晰的组织方式:SettingsTab.vue 负责挂载,useHomePageController.js 负责解析与状态,difficultylevelsGrouping.js 负责归组规则,settingsHelpers.js 负责保存入口,files.rs 负责实际落盘。整条链路在当前仓库中都能够定位到,没有依赖额外服务端。

相关推荐
Mr数据杨18 小时前
暗黑破坏神2 MOD修改工具装备编辑其他物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨2 天前
暗黑破坏神2 MOD修改工具装备编辑暗金物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨2 天前
暗黑破坏神2 MOD修改工具装备编辑武器物品
mod·游戏工具·暗黑破坏神2重置版
Mr数据杨2 天前
暗黑破坏神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