Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
febe90a2d1 | ||
|
|
0ecc020c23 | ||
|
|
fc3df13dd9 | ||
| 231349b24c |
188
README.md
188
README.md
@@ -1,124 +1,104 @@
|
||||
# 幸福工厂蓝图编辑器 - 项目文档
|
||||
# Satisfactory 蓝图编辑器文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个用于解析和编辑《幸福工厂》(Satisfactory)游戏蓝图(.sbp文件)的Web工具。用户可以上传蓝图文件,查看和修改其中的材料信息,然后下载修改后的蓝图文件。
|
||||
这个项目是一个用于《Satisfactory》游戏的蓝图编辑器,允许玩家上传、编辑和导出游戏蓝图文件(.sbp/.sbpcfg)。编辑器提供了JSON编辑和图形化编辑两种方式,让玩家可以轻松修改蓝图的各种属性。
|
||||
|
||||
## 文件结构解析
|
||||
## 功能特点
|
||||
|
||||
### 1. 文件头部 (32字节/64位十六进制)
|
||||
结构如下:
|
||||
```
|
||||
[4位] version - 版本号
|
||||
[4位] header_size - 头部大小(目前总是错误的)
|
||||
[4位] timestamp - 时间戳
|
||||
[4位] unknown1 - 未知字段1
|
||||
[4位] unknown2 - 未知字段2
|
||||
[4位] unknown3 - 未知字段3
|
||||
[4位] material_count - 材料数量
|
||||
[4位] reserved - 保留字段
|
||||
### 1. 蓝图文件导入
|
||||
- 支持拖放上传或点击上传
|
||||
- 支持.sbp(主蓝图文件)和.sbpcfg(配置文件)
|
||||
- 显示上传文件的基本信息
|
||||
|
||||
### 2. JSON编辑器
|
||||
- 将蓝图文件解析为可编辑的JSON格式
|
||||
- 支持格式化JSON
|
||||
- 可刷新和更新JSON数据
|
||||
- 可选择是否显示Objects段(包含大量实体数据)
|
||||
|
||||
### 3. 图形化编辑器
|
||||
- **基础编辑**:
|
||||
- 蓝图基本信息(名称、尺寸)
|
||||
- 配置信息(版本、图标、描述、颜色等)
|
||||
- 头部信息(版本号)
|
||||
- 物品消耗管理
|
||||
- 配方引用管理
|
||||
|
||||
- **进阶编辑**:
|
||||
- 实体列表浏览
|
||||
- 实体位置编辑(旋转、平移、缩放)
|
||||
- 属性编辑(添加/删除/修改属性)
|
||||
- 组件编辑
|
||||
- 附属组件编辑
|
||||
|
||||
### 4. 蓝图导出
|
||||
- 将编辑后的JSON导出为.sbp和.sbpcfg文件
|
||||
- 显示导出状态
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **前端框架**:Vue 3
|
||||
- **UI库**:DaisyUI (https://github.com/saadeghi/daisyui)
|
||||
- **蓝图解析库**:@etothepii/satisfactory-file-parser (https://github.com/etothepii4/satisfactory-file-parser)
|
||||
|
||||
- **图标库**:Font Awesome
|
||||
|
||||
## 安装与使用
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 材料部分
|
||||
每个材料的结构:
|
||||
### 开发模式
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
[4位] 路径长度(包括null结束符)
|
||||
[变长] 材料路径(以00结束)
|
||||
[4位] 数量
|
||||
[4位] 参数(只有最后一个材料有值)
|
||||
|
||||
### 生产构建
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
材料部分以`00000000`结束
|
||||
|
||||
### 3. 建筑部分
|
||||
每个建筑的结构:
|
||||
```
|
||||
[4位] 路径长度(包括null结束符)
|
||||
[变长] 建筑路径(以00结束)
|
||||
[4位] 参数(只有最后一个建筑有值)
|
||||
```
|
||||
建筑部分以`22222222`结束
|
||||
|
||||
### 4. 压缩数据部分
|
||||
Zlib压缩的数据,工具不做解析
|
||||
|
||||
## 功能说明
|
||||
|
||||
### 主要功能
|
||||
1. **文件上传**:支持.sbp蓝图文件上传
|
||||
2. **头部信息展示**:显示蓝图文件的头部信息
|
||||
3. **材料编辑**:
|
||||
- 查看所有材料及其属性
|
||||
- 修改材料路径和数量
|
||||
- 添加/删除材料
|
||||
4. **建筑信息展示**:显示建筑信息(只读)
|
||||
5. **数据下载**:将修改后的数据保存为.sbp文件
|
||||
|
||||
### 特殊处理
|
||||
1. 材料部分的最后一个条目必须有参数值
|
||||
2. 建筑部分的最后一个条目必须有参数值
|
||||
3. 自动更新材料数量计数
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 点击"选择文件"按钮上传.sbp蓝图文件
|
||||
2. 在"材料资产"部分可以:
|
||||
- 修改材料路径(需使用正确的游戏资源路径)
|
||||
- 修改材料数量
|
||||
- 添加新材料(点击"添加材料"按钮)
|
||||
- 删除材料(点击材料条目右侧的删除按钮)
|
||||
3. 修改完成后点击"下载蓝图"按钮保存文件
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 材料路径需要正确的游戏资源路径格式,可以参考其他蓝图文件中的路径
|
||||
2. 最后一个材料的参数值必须保留,不能删除
|
||||
3. 建筑信息目前是只读的,不能修改
|
||||
4. 压缩数据部分目前不做解析,下载时会原样保存
|
||||
|
||||
## 技术实现
|
||||
|
||||
- 前端框架:Vue.js
|
||||
- UI组件库:DaisyUI
|
||||
- 文件处理:使用JavaScript的FileReader和ArrayBuffer处理二进制数据
|
||||
- HEX转换:自定义方法实现字符串与十六进制的相互转换
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
├── index.html
|
||||
├── src/
|
||||
│ ├── App.vue # 主组件
|
||||
│ ├── main.js # 入口文件
|
||||
│ └── assets/ # 静态资源
|
||||
└── README.md # 项目说明
|
||||
src/
|
||||
├── components
|
||||
├── App.vue (主组件)
|
||||
└── main.js
|
||||
```
|
||||
|
||||
## 开发说明
|
||||
## 主要组件说明
|
||||
|
||||
1. 克隆仓库:
|
||||
```
|
||||
git clone https://git.liulikeji.cn/xingluo/satisfactory-BlueprintsEdit.git
|
||||
```
|
||||
2. 安装依赖:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
3. 运行开发服务器:
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
4. 构建生产版本:
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
### BlueprintEditor.vue
|
||||
|
||||
|
||||
## 未来计划
|
||||
这是应用的核心组件,包含所有蓝图编辑功能:
|
||||
|
||||
1. 添加建筑配方或超频编辑功能
|
||||
2. 支持压缩数据的解析和修改
|
||||
3. 添加材料图标
|
||||
#### 状态管理
|
||||
- `uploadedFiles`: 存储上传的文件
|
||||
- `blueprintData`: 存储解析后的蓝图数据
|
||||
- `rawObjects/newObjects`: 存储蓝图中的实体对象
|
||||
- `isParsing/isExporting`: 处理状态标志
|
||||
|
||||
## 贡献指南
|
||||
#### 主要方法
|
||||
- `parseBlueprint()`: 解析上传的蓝图文件
|
||||
- `exportBlueprint()`: 导出编辑后的蓝图
|
||||
- `formatJson()`: 格式化JSON显示
|
||||
- `updateDataFromJson()`: 从JSON更新数据
|
||||
|
||||
#### 实体编辑功能
|
||||
- `selectEntity()`: 选择要编辑的实体
|
||||
- `updateEntityFromJson()`: 更新实体数据
|
||||
- `addProperty()`: 添加新属性
|
||||
- `deleteProperty()`: 删除属性
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Objects段警告**:Objects段包含大量实体数据,对于大型蓝图,开启此选项可能导致性能问题。
|
||||
|
||||
2. **文件格式**:仅支持Satisfactory U8+版本的蓝图文件。
|
||||
|
||||
3. **数据安全**:编辑蓝图时建议备份原始文件,以防意外修改导致蓝图损坏。
|
||||
|
||||
欢迎提交Pull Request或Issue报告问题。对于新功能建议,请先创建Issue讨论。
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -12,6 +12,8 @@
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"axios": "^1.12.2",
|
||||
"daisyui": "^5.3.7",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-editor-vue3": "^1.0.4",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
@@ -1109,6 +1111,13 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
@@ -1310,6 +1319,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -1857,6 +1875,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1908,6 +1938,26 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.55.1",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor-vue3": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor-vue3/-/monaco-editor-vue3-1.0.4.tgz",
|
||||
"integrity": "sha512-gaIMBdhUGorOAX0kBvWul9QCQ+6J+MjZgqkieDECv3rjXsRbI07XNNrnD3IBC1jnGRF9+aTZ9CNhJ8Uv06z1uw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"vue": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"axios": "^1.12.2",
|
||||
"daisyui": "^5.3.7",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"monaco-editor-vue3": "^1.0.4",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
|
||||
397
src/App.vue
397
src/App.vue
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container mx-auto max-w-6xl p-4">
|
||||
<div class="">
|
||||
<div class="card bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-6">
|
||||
@@ -137,12 +137,15 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full h-96 mt-2 font-mono"
|
||||
v-model="displayedJson"
|
||||
placeholder="蓝图JSON内容将显示在这里..."
|
||||
:disabled="!isParsed"
|
||||
></textarea>
|
||||
<div class="h-96 border rounded-lg overflow-hidden">
|
||||
<CodeEditor
|
||||
v-model:value="displayedJson"
|
||||
language="json"
|
||||
theme="vs"
|
||||
:options="editorOptions"
|
||||
:disabled="!isParsed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 mt-4">
|
||||
<button
|
||||
@@ -534,8 +537,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 进阶编辑区域 - 使用第一个文件的编辑器 -->
|
||||
<div v-show="activeVisualTab === 'advanced' && showObjects">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<div v-show="activeVisualTab === 'advanced'">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-1">
|
||||
<!-- 左侧实体列表 -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
@@ -549,8 +552,8 @@
|
||||
:class="['p-3 mb-2 rounded-lg cursor-pointer hover:bg-base-300 transition',
|
||||
selectedEntityIndex === index ? 'bg-primary text-primary-content' : 'bg-base-200']"
|
||||
>
|
||||
<div class="font-bold truncate">{{ entity.instanceName || '未命名实体' }}</div>
|
||||
<div class="text-sm opacity-75 truncate">{{ entity.typePath || '未知类型' }}</div>
|
||||
<div class="font-bold truncate">{{ getDisplayName(entity.instanceName) || '未命名实体' }}</div>
|
||||
<div class="text-sm opacity-75 truncate">{{ getTypeName(entity.typePath) || '未知类型' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -562,7 +565,7 @@
|
||||
<div v-if="selectedEntity" class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title flex items-center">
|
||||
<span class="truncate">{{ selectedEntity.instanceName || '未命名实体' }}</span>
|
||||
<span class="truncate">{{ getDisplayName(selectedEntity.instanceName) || '未命名实体' }}</span>
|
||||
<span class="badge badge-primary ml-2">{{ selectedEntity.type || '未知类型' }}</span>
|
||||
</h2>
|
||||
|
||||
@@ -581,10 +584,14 @@
|
||||
<!-- 原始JSON标签页 -->
|
||||
<div v-if="activeTab === 'raw'">
|
||||
<h3 class="text-lg font-semibold mb-3">原始JSON数据</h3>
|
||||
<textarea
|
||||
v-model="entityJson"
|
||||
class="textarea textarea-bordered w-full h-96 font-mono"
|
||||
></textarea>
|
||||
<div class="h-96 border rounded-lg overflow-hidden">
|
||||
<CodeEditor
|
||||
v-model:value="entityJson"
|
||||
language="json"
|
||||
theme="vs"
|
||||
:options="editorOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<button @click="updateEntityFromJson" class="btn btn-primary">更新实体</button>
|
||||
</div>
|
||||
@@ -697,30 +704,28 @@
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="value.ueType" class="select select-bordered select-sm">
|
||||
<option value="FloatProperty">FloatProperty</option>
|
||||
<option value="IntProperty">IntProperty</option>
|
||||
<option value="ObjectProperty">ObjectProperty</option>
|
||||
<option value="StructProperty">StructProperty</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
v-model="value.ueType"
|
||||
class="input input-bordered input-sm"
|
||||
placeholder="输入类型"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-if="value.ueType === 'FloatProperty' || value.ueType === 'IntProperty'"
|
||||
type="number"
|
||||
v-model.number="value.value"
|
||||
class="input input-bordered input-sm"
|
||||
>
|
||||
<textarea
|
||||
v-else-if="value.ueType === 'ObjectProperty' || value.ueType === 'StructProperty'"
|
||||
v-model="objectProperties[key]"
|
||||
class="textarea textarea-bordered textarea-sm w-full"
|
||||
rows="3"
|
||||
></textarea>
|
||||
|
||||
<div v-if="isComplexValue(value.value)" class="h-20 border rounded-lg overflow-hidden">
|
||||
<CodeEditor
|
||||
v-model:value="objectProperties[key]"
|
||||
language="json"
|
||||
theme="vs"
|
||||
:options="smallEditorOptions"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
v-model="value.value"
|
||||
:value="getPropertyDisplayValue(value)"
|
||||
@input="setPropertyValue(value, $event.target.value)"
|
||||
class="input input-bordered input-sm"
|
||||
>
|
||||
</td>
|
||||
@@ -739,26 +744,13 @@
|
||||
<input type="text" v-model="newProperty.name" placeholder="属性名称" class="input input-bordered">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<select v-model="newProperty.type" class="select select-bordered">
|
||||
<option value="FloatProperty">FloatProperty</option>
|
||||
<option value="IntProperty">IntProperty</option>
|
||||
<option value="ObjectProperty">ObjectProperty</option>
|
||||
<option value="StructProperty">StructProperty</option>
|
||||
</select>
|
||||
<input type="text" v-model="newProperty.type" placeholder="属性类型" class="input input-bordered">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<input
|
||||
v-if="newProperty.type === 'FloatProperty' || newProperty.type === 'IntProperty'"
|
||||
type="number"
|
||||
v-model.number="newProperty.value"
|
||||
placeholder="值"
|
||||
class="input input-bordered"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
v-model="newProperty.value"
|
||||
placeholder="值"
|
||||
placeholder="属性值"
|
||||
class="input input-bordered"
|
||||
>
|
||||
</div>
|
||||
@@ -772,10 +764,14 @@
|
||||
<!-- 组件标签页 -->
|
||||
<div v-if="activeTab === 'components'">
|
||||
<h3 class="text-lg font-semibold mb-3">组件编辑</h3>
|
||||
<textarea
|
||||
v-model="componentsJson"
|
||||
class="textarea textarea-bordered w-full h-96 font-mono"
|
||||
></textarea>
|
||||
<div class="h-96 border rounded-lg overflow-hidden">
|
||||
<CodeEditor
|
||||
v-model:value="componentsJson"
|
||||
language="json"
|
||||
theme="vs"
|
||||
:options="editorOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<button @click="updateComponents" class="btn btn-primary">更新组件</button>
|
||||
</div>
|
||||
@@ -793,14 +789,18 @@
|
||||
>
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title font-semibold">
|
||||
{{ comp.instanceName ? comp.instanceName.split('.').pop() : '未命名组件' }}
|
||||
<span class="badge badge-sm badge-neutral ml-2">{{ comp.typePath ? comp.typePath.split('/').pop() : '未知类型' }}</span>
|
||||
{{ comp.instanceName ? getDisplayName(comp.instanceName).split('.').pop() : '未命名组件' }}
|
||||
<span class="badge badge-sm badge-neutral ml-2">{{ getTypeName(comp.typePath) || '未知类型' }}</span>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<textarea
|
||||
v-model="attachedComponentJson[index]"
|
||||
class="textarea textarea-bordered w-full h-64 font-mono mb-4"
|
||||
></textarea>
|
||||
<div class="h-64 border rounded-lg overflow-hidden mb-4">
|
||||
<CodeEditor
|
||||
v-model:value="attachedComponentJson[index]"
|
||||
language="json"
|
||||
theme="vs"
|
||||
:options="editorOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button @click="updateAttachedComponent(index)" class="btn btn-primary">更新组件</button>
|
||||
</div>
|
||||
@@ -832,13 +832,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Parser } from '@etothepii/satisfactory-file-parser';
|
||||
import { CodeEditor } from 'monaco-editor-vue3';
|
||||
|
||||
export default {
|
||||
name: 'BlueprintEditor',
|
||||
components: {
|
||||
CodeEditor
|
||||
},
|
||||
setup() {
|
||||
// 文件输入引用
|
||||
const fileInput = ref(null);
|
||||
@@ -855,6 +859,33 @@ export default {
|
||||
const activeVisualTab = ref('basic');
|
||||
const isParsed = ref(false);
|
||||
|
||||
// 默认的 sbpcfg 十六进制数据
|
||||
const defaultSbpcfgHex = '04000000e5ffffff5300610074006900730066006100630074006f00720079002000dd84fe56167f918f6856d89ea48b73006200700063006600670000000e0300000000803f0000803f0000803f0000803f300000002f47616d652f466163746f727947616d652f2d5368617265642f426c75657072696e742f49636f6e4c696272617279000c00000049636f6e4c69627261727900010000000000000000000000000000000000000000';
|
||||
|
||||
// 编辑器选项 - 调整行号宽度
|
||||
const editorOptions = ref({
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
folding: true,
|
||||
tabSize: 2,
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 8,
|
||||
horizontalScrollbarSize: 8
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const smallEditorOptions = ref({
|
||||
...editorOptions.value,
|
||||
fontSize: 12,
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
tabSize: 2,
|
||||
});
|
||||
|
||||
// 蓝图数据对象
|
||||
const blueprintData = ref({
|
||||
name: '',
|
||||
@@ -897,8 +928,8 @@ export default {
|
||||
});
|
||||
|
||||
// Objects 数据
|
||||
const rawObjects = ref([]); // 原始读取到的Objects
|
||||
const newObjects = ref([]); // 编辑后的Objects
|
||||
const rawObjects = ref([]);
|
||||
const newObjects = ref([]);
|
||||
|
||||
// 使用轻量级对象存储文件信息
|
||||
const fileInfo = ref({
|
||||
@@ -927,7 +958,7 @@ export default {
|
||||
|
||||
// 计算属性
|
||||
const canParse = computed(() => {
|
||||
return fileInfo.value.mainFileName; // 只需要主文件存在
|
||||
return fileInfo.value.mainFileName;
|
||||
});
|
||||
|
||||
const mainFileName = computed(() => fileInfo.value.mainFileName);
|
||||
@@ -982,10 +1013,8 @@ export default {
|
||||
get: () => {
|
||||
if (!isParsed.value) return '';
|
||||
|
||||
// 创建当前JSON的副本
|
||||
const jsonObj = {
|
||||
...blueprintData.value,
|
||||
// 根据showObjects决定是否包含objects
|
||||
objects: showObjects.value ? newObjects.value : undefined
|
||||
};
|
||||
|
||||
@@ -993,10 +1022,69 @@ export default {
|
||||
},
|
||||
set: (value) => {
|
||||
// 仅存储JSON字符串,不自动更新数据模型
|
||||
// 用户需要手动点击"更新数据"按钮来应用更改
|
||||
}
|
||||
});
|
||||
|
||||
// 辅助函数:检查是否为复杂对象需要JSON编辑器
|
||||
const isComplexValue = (value) => {
|
||||
if (value === null || value === undefined) return false;
|
||||
return typeof value === 'object' && !(value instanceof Array);
|
||||
};
|
||||
|
||||
// 辅助函数:获取属性显示类型
|
||||
const getPropertyDisplayType = (property) => {
|
||||
if (!property) return '';
|
||||
return property.ueType || property.type || '';
|
||||
};
|
||||
|
||||
// 辅助函数:获取属性值用于显示
|
||||
const getPropertyDisplayValue = (property) => {
|
||||
if (!property) return '';
|
||||
|
||||
if (isComplexValue(property.value)) {
|
||||
return JSON.stringify(property.value, null, 2);
|
||||
}
|
||||
|
||||
// 处理嵌套的value对象(如ByteProperty的情况)
|
||||
if (property.value && typeof property.value === 'object' && 'value' in property.value) {
|
||||
return property.value.value;
|
||||
}
|
||||
|
||||
return property.value;
|
||||
};
|
||||
|
||||
// 辅助函数:设置属性值
|
||||
const setPropertyValue = (property, newValue) => {
|
||||
if (!property) return;
|
||||
|
||||
try {
|
||||
// 如果是复杂对象,尝试解析JSON
|
||||
if (typeof newValue === 'string' && newValue.trim().startsWith('{')) {
|
||||
property.value = JSON.parse(newValue);
|
||||
} else if (property.value && typeof property.value === 'object' && 'value' in property.value) {
|
||||
// 处理嵌套value对象的情况(如ByteProperty)
|
||||
property.value.value = newValue;
|
||||
} else {
|
||||
property.value = newValue;
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果JSON解析失败,保持原值
|
||||
console.warn('JSON解析失败,保持原值:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 方法:处理显示名称
|
||||
const getDisplayName = (instanceName) => {
|
||||
if (!instanceName) return '';
|
||||
return instanceName.replace(/^Persistent_Level:PersistentLevel\./, '');
|
||||
};
|
||||
|
||||
// 方法:处理类型名称
|
||||
const getTypeName = (typePath) => {
|
||||
if (!typePath) return '';
|
||||
return typePath.split('/').pop() || typePath;
|
||||
};
|
||||
|
||||
// 方法
|
||||
const triggerFileInput = () => {
|
||||
fileInput.value.click();
|
||||
@@ -1024,7 +1112,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只存储文件名,不存储文件内容
|
||||
const sbpFile = validFiles.find(f => f.name.endsWith('.sbp'));
|
||||
const sbpcfgFile = validFiles.find(f => f.name.endsWith('.sbpcfg'));
|
||||
|
||||
@@ -1034,7 +1121,6 @@ export default {
|
||||
configFileName: sbpcfgFile ? sbpcfgFile.name : ''
|
||||
};
|
||||
|
||||
// 只存储文件引用,不存储内容
|
||||
uploadedFiles.value = validFiles;
|
||||
};
|
||||
|
||||
@@ -1064,7 +1150,6 @@ export default {
|
||||
if (!canParse.value || isParsing.value) return;
|
||||
|
||||
try {
|
||||
// 重置状态
|
||||
isParsing.value = true;
|
||||
statusMessages.value = [];
|
||||
progress.value = 0;
|
||||
@@ -1074,11 +1159,9 @@ export default {
|
||||
rawObjects.value = [];
|
||||
newObjects.value = [];
|
||||
|
||||
// 获取主文件
|
||||
const sbpFile = uploadedFiles.value.find(f => f.name.endsWith('.sbp'));
|
||||
const sbpcfgFile = uploadedFiles.value.find(f => f.name.endsWith('.sbpcfg'));
|
||||
|
||||
// 读取文件为ArrayBuffer
|
||||
const readFileAsArrayBuffer = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -1095,13 +1178,18 @@ export default {
|
||||
sbpcfgBuffer = await readFileAsArrayBuffer(sbpcfgFile);
|
||||
statusMessages.value.push('检测到配置文件,将一起解析...');
|
||||
} else {
|
||||
statusMessages.value.push('未检测到配置文件,将仅解析主文件...');
|
||||
statusMessages.value.push('未检测到配置文件,将使用默认配置...');
|
||||
const hexString = defaultSbpcfgHex;
|
||||
const buffer = new ArrayBuffer(hexString.length / 2);
|
||||
const view = new Uint8Array(buffer);
|
||||
for (let i = 0; i < hexString.length; i += 2) {
|
||||
view[i / 2] = parseInt(hexString.substring(i, i + 2), 16);
|
||||
}
|
||||
sbpcfgBuffer = buffer;
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
statusMessages.value.push('开始解析蓝图文件...');
|
||||
|
||||
// 解析蓝图
|
||||
const blueprint = await Parser.ParseBlueprintFiles(
|
||||
fileInfo.value.blueprintName,
|
||||
sbpBuffer,
|
||||
@@ -1114,13 +1202,9 @@ export default {
|
||||
}
|
||||
);
|
||||
|
||||
// 存储原始objects数据
|
||||
rawObjects.value = blueprint.objects || [];
|
||||
|
||||
// 初始化newObjects为rawObjects的副本
|
||||
newObjects.value = JSON.parse(JSON.stringify(rawObjects.value));
|
||||
|
||||
// 提取蓝图数据
|
||||
blueprintData.value = {
|
||||
name: blueprint.name || '',
|
||||
compressionInfo: {
|
||||
@@ -1183,20 +1267,32 @@ export default {
|
||||
isExporting.value = true;
|
||||
exportStatus.value = '开始导出蓝图文件...';
|
||||
|
||||
// 创建当前JSON对象
|
||||
const currentJson = {
|
||||
...blueprintData.value,
|
||||
// 根据showObjects状态决定使用哪个objects数据
|
||||
objects: showObjects.value ? newObjects.value : rawObjects.value
|
||||
};
|
||||
// 创建当前时间戳
|
||||
const timestamp = Date.now().toString();
|
||||
|
||||
// 准备导出的数据 - 深拷贝避免修改原始数据
|
||||
const exportData = JSON.parse(JSON.stringify({
|
||||
...blueprintData.value,
|
||||
objects: newObjects.value
|
||||
}));
|
||||
|
||||
// 确保 lastEditedBy 数组存在
|
||||
if (!exportData.config.lastEditedBy) {
|
||||
exportData.config.lastEditedBy = [];
|
||||
}
|
||||
|
||||
// 在数组尾部添加新的编辑者信息(第二位)
|
||||
exportData.config.lastEditedBy.push({
|
||||
accountId: timestamp,
|
||||
displayName: "LinXingLuo",
|
||||
platformName: "Satisfactory-edit"
|
||||
});
|
||||
|
||||
// 准备导出变量
|
||||
let mainFileHeader = null;
|
||||
const mainFileBodyChunks = [];
|
||||
|
||||
// 导出蓝图
|
||||
const summary = await Parser.WriteBlueprintFiles(
|
||||
currentJson,
|
||||
exportData,
|
||||
header => {
|
||||
mainFileHeader = header;
|
||||
},
|
||||
@@ -1205,13 +1301,11 @@ export default {
|
||||
}
|
||||
);
|
||||
|
||||
// 组合主文件
|
||||
const mainFileData = new Uint8Array([
|
||||
...mainFileHeader,
|
||||
...mainFileBodyChunks.flatMap(chunk => [...chunk])
|
||||
]);
|
||||
|
||||
// 创建下载链接
|
||||
const downloadFile = (data, filename, type) => {
|
||||
const blob = new Blob([data], { type });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -1224,7 +1318,6 @@ export default {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
downloadFile(mainFileData, `${blueprintName.value}.sbp`, 'application/octet-stream');
|
||||
downloadFile(summary.configFileBinary, `${blueprintName.value}.sbpcfg`, 'application/octet-stream');
|
||||
|
||||
@@ -1247,9 +1340,7 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新JSON显示
|
||||
const refreshJson = () => {
|
||||
// 创建一个新的JSON对象,根据showObjects状态决定是否包含objects
|
||||
const jsonObj = {
|
||||
...blueprintData.value,
|
||||
objects: showObjects.value ? newObjects.value : undefined
|
||||
@@ -1258,19 +1349,16 @@ export default {
|
||||
displayedJson.value = JSON.stringify(jsonObj, null, 2);
|
||||
};
|
||||
|
||||
// 从JSON更新数据
|
||||
const updateDataFromJson = () => {
|
||||
try {
|
||||
const editedObj = JSON.parse(displayedJson.value);
|
||||
|
||||
// 更新blueprintData(除了objects)
|
||||
for (const key in editedObj) {
|
||||
if (key !== 'objects') {
|
||||
blueprintData.value[key] = editedObj[key];
|
||||
}
|
||||
}
|
||||
|
||||
// 如果JSON中包含objects字段,则更新newObjects
|
||||
if (editedObj.objects) {
|
||||
newObjects.value = editedObj.objects;
|
||||
}
|
||||
@@ -1293,7 +1381,6 @@ export default {
|
||||
const updateEntityFromJson = () => {
|
||||
try {
|
||||
const updatedEntity = JSON.parse(entityJson.value);
|
||||
// 找到实体在原始数组中的位置
|
||||
const originalIndex = newObjects.value.findIndex(
|
||||
e => e.instanceName === selectedEntity.value.instanceName
|
||||
);
|
||||
@@ -1344,17 +1431,6 @@ export default {
|
||||
const addProperty = () => {
|
||||
if (!newProperty.value.name || !selectedEntity.value) return;
|
||||
|
||||
// 处理ObjectProperty类型
|
||||
let value = newProperty.value.value;
|
||||
if (newProperty.value.type === 'ObjectProperty') {
|
||||
try {
|
||||
value = JSON.parse(newProperty.value.value);
|
||||
} catch (e) {
|
||||
value = { levelName: "", pathName: "" };
|
||||
}
|
||||
}
|
||||
|
||||
// 确保properties对象存在
|
||||
if (!selectedEntity.value.properties) {
|
||||
selectedEntity.value.properties = {};
|
||||
}
|
||||
@@ -1363,15 +1439,13 @@ export default {
|
||||
type: newProperty.value.type,
|
||||
ueType: newProperty.value.type,
|
||||
name: newProperty.value.name,
|
||||
value: value
|
||||
value: newProperty.value.value
|
||||
};
|
||||
|
||||
// 初始化新属性的JSON字符串
|
||||
if (newProperty.value.type === 'ObjectProperty' || newProperty.value.type === 'StructProperty') {
|
||||
objectProperties.value[newProperty.value.name] = JSON.stringify(value, null, 2);
|
||||
if (isComplexValue(newProperty.value.value)) {
|
||||
objectProperties.value[newProperty.value.name] = JSON.stringify(newProperty.value.value, null, 2);
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
newProperty.value = {
|
||||
name: '',
|
||||
type: 'FloatProperty',
|
||||
@@ -1385,7 +1459,7 @@ export default {
|
||||
objectProperties.value = {};
|
||||
for (const key in selectedEntity.value.properties) {
|
||||
const prop = selectedEntity.value.properties[key];
|
||||
if (prop.ueType === 'ObjectProperty' || prop.ueType === 'StructProperty') {
|
||||
if (isComplexValue(prop.value)) {
|
||||
objectProperties.value[key] = JSON.stringify(prop.value, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -1422,7 +1496,6 @@ export default {
|
||||
|
||||
// 监听newObjects变化
|
||||
watch(newObjects, () => {
|
||||
// 当newObjects变化时,刷新附属组件
|
||||
if (selectedEntity.value) {
|
||||
initAttachedComponentJson();
|
||||
}
|
||||
@@ -1447,6 +1520,8 @@ export default {
|
||||
mainFileName,
|
||||
configFileName,
|
||||
isParsed,
|
||||
editorOptions,
|
||||
smallEditorOptions,
|
||||
|
||||
// 第一个文件编辑器相关
|
||||
filteredEntities,
|
||||
@@ -1461,6 +1536,12 @@ export default {
|
||||
componentsJson,
|
||||
attachedComponents,
|
||||
|
||||
// 辅助函数
|
||||
isComplexValue,
|
||||
getPropertyDisplayType,
|
||||
getPropertyDisplayValue,
|
||||
setPropertyValue,
|
||||
|
||||
// 方法
|
||||
triggerFileInput,
|
||||
handleFileChange,
|
||||
@@ -1475,6 +1556,8 @@ export default {
|
||||
removeRecipe,
|
||||
addItemCost,
|
||||
removeItemCost,
|
||||
getDisplayName,
|
||||
getTypeName,
|
||||
|
||||
// 第一个文件编辑器方法
|
||||
selectEntity,
|
||||
@@ -1486,86 +1569,4 @@ export default {
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.json-editor {
|
||||
min-height: 400px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.large-file-warning {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
|
||||
}
|
||||
|
||||
.dimension-input {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.recipe-item {
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-cost {
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.editor-tabs .tab {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.tab[disabled] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.object-list-item {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.object-list-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
@@ -1,43 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -1,2 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
@plugin "daisyui";
|
||||
|
||||
Reference in New Issue
Block a user