第一次提交

This commit is contained in:
HKXluo
2025-10-18 00:48:16 +08:00
parent c68bad4f1a
commit 1bac61f9fc
7 changed files with 577 additions and 0 deletions

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

518
src/App.vue Normal file
View File

@@ -0,0 +1,518 @@
<template>
<div class="navbar bg-base-100 shadow-sm">
<div class="flex-none">
<button class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block h-5 w-5 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</div>
<div class="flex-1">
<input type="file" id="fileInput" class="file-input" @change="handleFileUpload" accept=".sbp"/>
</div>
<div class="flex-none" v-if="parsedData">
<button class="btn btn-ghost" @click="downloadCombinedData">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
下载蓝图(.sbp)
</button>
</div>
</div>
<div class="container mx-auto p-4">
<div v-if="error" class="alert alert-error mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ error }}</span>
</div>
<div v-if="loading" class="flex flex-col items-center justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
<p class="mt-4">正在解析蓝图文件...</p>
</div>
<div v-if="parsedData" class="space-y-4">
<!-- 文件头信息 -->
<div class="collapse collapse-arrow bg-base-200 border border-base-300 rounded-box">
<input type="checkbox" class="peer" checked />
<div class="collapse-title font-medium">
<span>文件头信息</span>
</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table table-zebra">
<tbody>
<tr v-for="(value, key) in parsedData.header" :key="key">
<th class="w-1/3">{{ key }}</th>
<td>{{ value }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 材料资产 -->
<div class="collapse collapse-arrow bg-base-200 border border-base-300 rounded-box">
<input type="checkbox" class="peer" checked />
<div class="collapse-title font-medium">
<span>材料资产 ({{ parsedData.materials.length }})</span>
</div>
<div class="collapse-content space-y-4">
<div v-for="(mat, index) in parsedData.materials" :key="index" class="bg-base-100 p-4 rounded-box">
<div class="flex gap-2 items-end">
<div class="form-control flex-1">
<label class="label">
<div class="tooltip tooltip-right" data-tip="此部分表达比较抽象如果你不知道那个资源的路径是什么可以找一个有这个资源的蓝图文件解析后复制过来">
<span class="label-text">路径 </span>
</div>
</label>
<input
type="text"
v-model="mat.path"
class="input input-bordered w-full"
@input="updateMaterial(index, 'path', mat.path)"
>
</div>
<div class="form-control w-24">
<label class="label">
<span class="label-text">数量</span>
</label>
<input
type="number"
v-model.number="mat.amount"
class="input input-bordered"
@input="updateMaterial(index, 'amount', mat.amount)"
>
</div>
<div class="form-control w-24">
<label class="label">
<div class="tooltip tooltip-left tooltip-warning" data-tip="此数值只蓝图的最后一个才有且绑定不得修改如果删除此条则最后一个材料依然需要此数值">
<span class="label-text">属性 </span>
</div>
</label>
<input
type="text"
v-model="mat.attribute"
class="input input-bordered"
@input="updateMaterial(index, 'attribute', mat.attribute)"
>
</div>
<button @click="removeMaterial(index)" class="btn btn-square btn-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<button @click="addMaterial" class="btn btn-primary w-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
添加材料
</button>
</div>
</div>
<!-- 配方资产 -->
<div class="collapse collapse-arrow bg-base-200 border border-base-300 rounded-box">
<input type="checkbox" class="peer" />
<div class="collapse-title font-medium">
<span>配方资产 ({{ parsedData.recipes.length }})</span>
</div>
<div class="collapse-content">
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>路径</th>
<th>属性</th>
</tr>
</thead>
<tbody>
<tr v-for="(recipe, index) in parsedData.recipes" :key="index">
<td>{{ recipe.path }}</td>
<td>{{ recipe.attribute }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 高级数据 -->
<div class="collapse collapse-arrow bg-base-200 border border-base-300 rounded-box">
<input type="checkbox" class="peer" />
<div class="collapse-title font-medium">
<span>高级数据</span>
</div>
<div class="collapse-content space-y-4">
<div class="mockup-code">
<pre data-prefix=">"><code>元数据:</code></pre>
<pre data-prefix=">"><code>{{ parsedData.metadata_hex || '无元数据' }}</code></pre>
</div>
<div class="mockup-code">
<pre data-prefix=">"><code>压缩数据:</code></pre>
<pre data-prefix=">"><code>{{ parsedData.compressed_data_hex || '无压缩数据' }}</code></pre>
</div>
<div class="mockup-code">
<pre data-prefix=">"><code>原始JSON数据</code></pre>
<pre><code>{{ parsedData }}</code></pre>
</div>
</div>
</div>
<!-- 合并数据 -->
<div class="collapse collapse-arrow bg-base-200 border border-base-300 rounded-box">
<input type="checkbox" class="peer" />
<div class="collapse-title font-medium">
<span>合并数据 ({{ combinedData ? combinedData.length/2 : 0 }}字节)</span>
</div>
<div class="collapse-content">
<div class="mockup-code">
<pre data-prefix=">"><code>合并后的HEX数据</code></pre>
<pre><code>{{ combinedData || '无数据' }}</code></pre>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
selectedFile: null,
parsedData: null,
loading: false,
error: null,
combinedData: null
};
},
methods: {
updateMaterial(index, field, value) {
this.parsedData.materials[index][field] = value;
this.updateMaterialRawData(index);
this.combineData();
},
updateMaterialRawData(index) {
const mat = this.parsedData.materials[index];
// 更新raw_path
mat.raw_path = this.stringToHex(mat.path) + "00";
// 更新raw_data_hex
const pathHex = this.stringToHex(mat.path);
const pathLengthHex = this.intToHexLE(pathHex.length / 2 + 1); // +1 for null terminator
const amountHex = this.intToHexLE(mat.amount);
const attributeHex = this.intToHexLE(mat.attribute);
mat.raw_data_hex = pathLengthHex.slice(0, 8) + pathHex + "00" + amountHex.slice(0, 8) + attributeHex.slice(0, 8);
},
addMaterial() {
const newMaterial = {
path: "",
amount: 0,
attribute: 0,
raw_path: "",
raw_data_hex: ""
};
this.parsedData.materials.push(newMaterial);
this.updateMaterialRawData(this.parsedData.materials.length - 1);
this.updateHeaderCount();
this.combineData();
},
removeMaterial(index) {
this.parsedData.materials.splice(index, 1);
this.updateHeaderCount();
this.combineData();
},
updateHeaderCount() {
if (!this.parsedData?.header) return;
// 更新header中的material_count
this.parsedData.header.material_count = this.parsedData.materials.length;
// 更新header_raw_hex中的material_count (位于第7个int32偏移24字节)
if (this.parsedData.header_raw_hex) {
try {
const headerBytes = this.hexToBytes(this.parsedData.header_raw_hex);
const view = new DataView(headerBytes.buffer);
view.setUint32(24, this.parsedData.materials.length, true);
this.parsedData.header_raw_hex = Array.from(new Uint8Array(headerBytes.buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
} catch (e) {
console.error('更新header_raw_hex失败:', e);
this.error = '更新文件头数据失败';
}
}
},
stringToHex(str) {
let hex = "";
for (let i = 0; i < str.length; i++) {
hex += str.charCodeAt(i).toString(16).padStart(2, '0');
}
return hex;
},
intToHexLE(num, bytes = 4) {
let hex = "";
for (let i = 0; i < bytes; i++) {
hex += ((num >> (i * 8)) & 0xFF).toString(16).padStart(2, '0');
}
return hex;
},
hexToBytes(hexString) {
const cleanHex = hexString.replace(/\s/g, '');
if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {
throw new Error('无效的HEX字符串');
}
if (cleanHex.length % 2 !== 0) {
throw new Error('HEX字符串长度应为偶数');
}
const bytes = new Uint8Array(cleanHex.length / 2);
for (let i = 0; i < cleanHex.length; i += 2) {
bytes[i/2] = parseInt(cleanHex.substr(i, 2), 16);
}
return bytes;
},
combineData() {
if (!this.parsedData) {
this.combinedData = null;
return '';
}
let combined = '';
// 添加header_raw_hex
if (this.parsedData.header_raw_hex) {
combined += this.parsedData.header_raw_hex;
}
// 添加所有材料的raw_data_hex
if (this.parsedData.materials && this.parsedData.materials.length > 0) {
this.parsedData.materials.forEach(mat => {
if (mat.raw_data_hex) {
combined += mat.raw_data_hex;
}
});
}
combined += "00000000"; // 材料数据结束标志
// 添加所有配方的raw_data_hex
if (this.parsedData.recipes && this.parsedData.recipes.length > 0) {
this.parsedData.recipes.forEach(recipe => {
if (recipe.raw_data_hex) {
combined += recipe.raw_data_hex;
}
});
}
combined += "22222222"; // 配方数据结束标志
// 添加压缩数据
if (this.parsedData.compressed_data_hex) {
combined += this.parsedData.compressed_data_hex;
}
this.combinedData = combined;
return combined;
},
downloadCombinedData() {
try {
const data = this.combinedData || this.combineData();
if (!data) {
this.error = '没有可下载的数据';
return;
}
const bytes = this.hexToBytes(data);
const blob = new Blob([bytes], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'blueprint.sbp';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
this.error = '下载失败: ' + error.message;
console.error('下载错误:', error);
}
},
handleFileUpload(event) {
this.selectedFile = event.target.files[0];
this.parsedData = null;
this.combinedData = null;
this.error = null;
if (this.selectedFile) {
this.parseBlueprint();
}
},
async parseBlueprint() {
if (!this.selectedFile) return;
this.loading = true;
this.error = null;
this.parsedData = null;
this.combinedData = null;
try {
const arrayBuffer = await this.readFileAsArrayBuffer(this.selectedFile);
this.parsedData = this.parseBinaryData(arrayBuffer);
this.combineData();
} catch (error) {
console.error(error);
this.error = '解析蓝图文件时出错: ' + error.message;
} finally {
this.loading = false;
}
},
readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
},
parseBinaryData(arrayBuffer) {
const view = new DataView(arrayBuffer);
let offset = 0;
// 1. 解析头部 (32字节)
const header = {
version: view.getUint32(offset, true),
header_size: view.getUint32(offset + 4, true),
timestamp: view.getUint32(offset + 8, true),
unknown1: view.getUint32(offset + 12, true),
unknown2: view.getUint32(offset + 16, true),
unknown3: view.getUint32(offset + 20, true),
material_count: view.getUint32(offset + 24, true),
reserved: view.getUint32(offset + 28, true)
};
offset += 32;
// 保存原始头部数据
const headerBytes = new Uint8Array(arrayBuffer.slice(0, 32));
const header_raw_hex = Array.from(headerBytes).map(b => b.toString(16).padStart(2, '0')).join('');
// 2. 解析材料部分
const materials = [];
for (let i = 0; i < header.material_count; i++) {
const pathLength = view.getUint32(offset, true);
offset += 4;
// 读取路径字符串
const pathBytes = new Uint8Array(arrayBuffer.slice(offset, offset + pathLength - 1));
const path = new TextDecoder().decode(pathBytes);
offset += pathLength;
const amount = view.getUint32(offset, true);
const attribute = view.getUint32(offset + 4, true);
offset += 8;
// 生成原始数据HEX表示
const pathHex = Array.from(pathBytes).map(b => b.toString(16).padStart(2, '0')).join('');
const pathLengthHex = this.intToHexLE(pathLength, 4);
const amountHex = this.intToHexLE(amount, 4);
const attributeHex = this.intToHexLE(attribute, 4);
materials.push({
path,
amount,
attribute,
raw_path: pathHex + "00",
raw_data_hex: pathLengthHex + pathHex + "00" + amountHex + attributeHex
});
}
// 检查材料部分结束标记 (4字节0)
const materialEnd = view.getUint32(offset, true);
offset += 4;
// 3. 解析建筑部分
const recipes = [];
while (true) {
// 检查是否到达建筑部分结束标记
if (view.getUint32(offset, true) === 0x22222222) {
offset += 4;
break;
}
const pathLength = view.getUint32(offset, true);
offset += 4;
// 读取路径字符串
const pathBytes = new Uint8Array(arrayBuffer.slice(offset, offset + pathLength - 1));
const path = new TextDecoder().decode(pathBytes);
offset += pathLength;
const attribute = view.getUint32(offset, true);
offset += 4;
// 生成原始数据HEX表示
const pathHex = Array.from(pathBytes).map(b => b.toString(16).padStart(2, '0')).join('');
const pathLengthHex = this.intToHexLE(pathLength, 4);
const attributeHex = this.intToHexLE(attribute, 4);
recipes.push({
path,
attribute,
raw_data_hex: pathLengthHex + pathHex + "00" + attributeHex
});
}
// 4. 读取压缩数据
const compressedData = new Uint8Array(arrayBuffer.slice(offset));
const compressed_data_hex = Array.from(compressedData).map(b => b.toString(16).padStart(2, '0')).join('');
return {
header,
header_raw_hex,
materials,
recipes,
compressed_data_hex,
compressed_data_size: compressedData.length
};
}
}
};
</script>
<style scoped>
/* 使用 DaisyUI 的类,无需额外样式 */
</style>

1
src/assets/vue.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,43 @@
<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>

5
src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

2
src/style.css Normal file
View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@plugin "daisyui";

7
vite.config.js Normal file
View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(),tailwindcss()],
})