@jjlmoya/utils-games-development 1.51.0 → 1.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/saveFileEditor/bibliography.astro +6 -0
- package/src/tool/saveFileEditor/bibliography.ts +12 -0
- package/src/tool/saveFileEditor/component.astro +351 -0
- package/src/tool/saveFileEditor/entry.ts +28 -0
- package/src/tool/saveFileEditor/game-save-file-editor.css +250 -0
- package/src/tool/saveFileEditor/i18n/de.ts +182 -0
- package/src/tool/saveFileEditor/i18n/en.ts +182 -0
- package/src/tool/saveFileEditor/i18n/es.ts +182 -0
- package/src/tool/saveFileEditor/i18n/fr.ts +182 -0
- package/src/tool/saveFileEditor/i18n/id.ts +182 -0
- package/src/tool/saveFileEditor/i18n/it.ts +182 -0
- package/src/tool/saveFileEditor/i18n/ja.ts +182 -0
- package/src/tool/saveFileEditor/i18n/ko.ts +182 -0
- package/src/tool/saveFileEditor/i18n/nl.ts +182 -0
- package/src/tool/saveFileEditor/i18n/pl.ts +182 -0
- package/src/tool/saveFileEditor/i18n/pt.ts +182 -0
- package/src/tool/saveFileEditor/i18n/ru.ts +182 -0
- package/src/tool/saveFileEditor/i18n/sv.ts +182 -0
- package/src/tool/saveFileEditor/i18n/tr.ts +182 -0
- package/src/tool/saveFileEditor/i18n/zh.ts +182 -0
- package/src/tool/saveFileEditor/index.ts +11 -0
- package/src/tool/saveFileEditor/logic.test.ts +69 -0
- package/src/tool/saveFileEditor/logic.ts +139 -0
- package/src/tool/saveFileEditor/seo.astro +15 -0
- package/src/tool/saveFileEditor/ui.ts +24 -0
- package/src/tools.ts +2 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { SaveFileEditorUI } from '../ui';
|
|
3
|
+
import { bibliographyEntries } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
export const content: ToolLocaleContent<SaveFileEditorUI> = {
|
|
6
|
+
slug: 'game-save-file-editor',
|
|
7
|
+
title: '游戏存档文件混淆与在线编辑器',
|
|
8
|
+
description: '在浏览器本地100%安全地解密、检查、编辑JSON结构并使用Base64、XOR掩码或明文重新加密游戏存档文件。',
|
|
9
|
+
ui: {
|
|
10
|
+
title: '游戏存档文件混淆与在线编辑器',
|
|
11
|
+
subtitle: '无需上传服务器,安全地检查、修改和加密本地游戏存档状态文件',
|
|
12
|
+
dropSaveFile: '将游戏存档文件拖放到此处',
|
|
13
|
+
orSelectFile: '或点击浏览本地文件',
|
|
14
|
+
encryptionMethod: '加密格式',
|
|
15
|
+
methodBase64: 'Base64 编码',
|
|
16
|
+
methodXor: 'XOR 掩码 + Base64',
|
|
17
|
+
methodRaw: '明文 JSON / 未加密',
|
|
18
|
+
xorKeyLabel: 'XOR 密钥',
|
|
19
|
+
xorKeyPlaceholder: '例如 MySecretGameKey2026',
|
|
20
|
+
jsonRawTitle: '已解密的 JSON 数据(实时编辑器)',
|
|
21
|
+
encodeAndDownload: '加密并下载存档文件',
|
|
22
|
+
copyEncoded: '复制加密文本',
|
|
23
|
+
copiedNotice: '已复制到剪贴板!',
|
|
24
|
+
decodedKeysCount: '参数总数',
|
|
25
|
+
dataSize: '数据大小',
|
|
26
|
+
detectedFormat: '检测到的格式',
|
|
27
|
+
exportPreviewLabel: '加密输出预览',
|
|
28
|
+
decodePanelTitle: '解密与实时 JSON 编辑器',
|
|
29
|
+
exportPanelTitle: '重新加密输出数据',
|
|
30
|
+
decodeError: '无法解密游戏存档文件',
|
|
31
|
+
bytesUnit: '字节',
|
|
32
|
+
},
|
|
33
|
+
seo: [
|
|
34
|
+
{
|
|
35
|
+
type: 'title',
|
|
36
|
+
level: 2,
|
|
37
|
+
text: '游戏存档文件安全与混淆协议',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'paragraph',
|
|
41
|
+
html: '在电子游戏开发中,玩家的进度、道具数量、解锁关卡和角色属性等状态数据通常会被序列化并持久化存储。为了防止普通玩家通过文本编辑器随意篡改存档,游戏开发团队通常会使用 Base64 编码或基于自定义密钥的按位 XOR 掩码对存档文件进行混淆。在内部 QA 测试和在线运营调试过程中,开发人员需要能够实时查看原始 JSON 结构、设定边界测试状态,并在无需重新编译游戏二进制文件的情况下重新加密导出存档。',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'stats',
|
|
45
|
+
columns: 4,
|
|
46
|
+
items: [
|
|
47
|
+
{ label: '客户端处理隐私', value: '100% 本地' },
|
|
48
|
+
{ label: '支持的解密方式', value: 'Base64 / XOR / JSON' },
|
|
49
|
+
{ label: '解密延迟', value: '0 毫秒' },
|
|
50
|
+
{ label: '数据泄漏风险', value: '零' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'title',
|
|
55
|
+
level: 2,
|
|
56
|
+
text: '混淆方案对比',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: 'comparative',
|
|
60
|
+
columns: 3,
|
|
61
|
+
items: [
|
|
62
|
+
{
|
|
63
|
+
title: 'Base64 编码',
|
|
64
|
+
description: '轻量级的文本转换格式,可防止在记事本中直接修改,但不具备真正的密码学安全性。',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: 'XOR 掩码 + Base64',
|
|
68
|
+
description: '独立游戏开发中的标准做法。将文本字节与密钥混合,有效防御内存修改器和简易作弊工具。',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: '明文 JSON 数据',
|
|
72
|
+
description: '未加密的可读存档状态。非常适合早期原型设计和内部团队快速迭代。',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'title',
|
|
78
|
+
level: 2,
|
|
79
|
+
text: '存档状态验证的 QA 测试实践',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'tip',
|
|
83
|
+
title: 'QA 测试中的存档安全最佳实践',
|
|
84
|
+
html: '请务必为内部测试构建与生产构建配置不同的调试密钥。验证边界条件 Bug 时,可使用本地树状编辑器强行设置道具上限和测试数值,无需重新编译代码。',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'title',
|
|
88
|
+
level: 2,
|
|
89
|
+
text: '游戏状态参数指南表',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'table',
|
|
93
|
+
headers: ['数据类型', '推荐格式', '常见应用场景', '混淆层'],
|
|
94
|
+
rows: [
|
|
95
|
+
['数值整数', '32 位整数', '金币、等级、经验值、弹药', 'XOR 掩码'],
|
|
96
|
+
['布尔标志', '标准布尔值', '新手教程完成、Boss 击败', 'Base64 / XOR'],
|
|
97
|
+
['嵌套对象', 'JSON 层次结构', '玩家背包、技能树', 'Base64 编码'],
|
|
98
|
+
['时间戳字符串', 'ISO 8601 UTC', '每日签到、存档时间戳', 'XOR 掩码'],
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: 'title',
|
|
103
|
+
level: 2,
|
|
104
|
+
text: '逆向工程与防篡改考量',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'paragraph',
|
|
108
|
+
html: '尽管客户端混淆可以防止普通玩家篡改存档,但 XOR 掩码和 Base64 编码并非强加密算法。使用 RenderDoc 或 x64dbg 等内存调试工具可以从编译好的二进制文件中分析出密钥生成逻辑。对于竞技类多人游戏,必须结合服务器端权威验证或 HMAC 数字签名来防止篡改。',
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
faqTitle: '常见问题解答',
|
|
112
|
+
faq: [
|
|
113
|
+
{
|
|
114
|
+
question: '我的游戏存档文件会被上传到远程服务器吗?',
|
|
115
|
+
answer: '不会。所有解密、JSON 编辑和重新加密过程 100% 在您的 Web 浏览器 JavaScript 引擎内部本地完成。',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
question: 'XOR 密钥混淆在 Unity 或 Godot 等游戏引擎中是如何工作的?',
|
|
119
|
+
answer: 'XOR 混淆会遍历序列化 JSON 字符串的 UTF-8 字节,将其与密钥字符逐字节进行按位 XOR 异或运算,再将结果转换为 Base64 编码。',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
howTo: [
|
|
123
|
+
{
|
|
124
|
+
name: '加载或粘贴存档文件',
|
|
125
|
+
text: '上传加密的游戏存档文件或将文件拖入加载区域。',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: '选择解密方法和密钥',
|
|
129
|
+
text: '选择 Base64 或 XOR 掩码,并输入对应的游戏秘钥。',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: '编辑 JSON 状态',
|
|
133
|
+
text: '在实时编辑器中调整等级、金币、道具或游戏状态标志。',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: '加密并导出',
|
|
137
|
+
text: '选择目标输出格式并下载修改后的存档文件以供游戏测试。',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
schemas: [
|
|
141
|
+
{
|
|
142
|
+
'@context': 'https://schema.org',
|
|
143
|
+
'@type': 'SoftwareApplication',
|
|
144
|
+
name: '游戏存档文件编辑器',
|
|
145
|
+
applicationCategory: 'DeveloperApplication',
|
|
146
|
+
operatingSystem: 'Any',
|
|
147
|
+
offers: {
|
|
148
|
+
'@type': 'Offer',
|
|
149
|
+
price: '0',
|
|
150
|
+
priceCurrency: 'CNY',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
'@context': 'https://schema.org',
|
|
155
|
+
'@type': 'FAQPage',
|
|
156
|
+
mainEntity: [
|
|
157
|
+
{
|
|
158
|
+
'@type': 'Question',
|
|
159
|
+
name: '我的游戏存档文件会被上传到远程服务器吗?',
|
|
160
|
+
acceptedAnswer: {
|
|
161
|
+
'@type': 'Answer',
|
|
162
|
+
text: '不会。所有处理过程 100% 在您的浏览器内部完成。',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
'@context': 'https://schema.org',
|
|
169
|
+
'@type': 'HowTo',
|
|
170
|
+
name: '如何编辑加密的游戏存档文件',
|
|
171
|
+
step: [
|
|
172
|
+
{
|
|
173
|
+
'@type': 'HowToStep',
|
|
174
|
+
name: '加载存档文件',
|
|
175
|
+
text: '上传加密的游戏存档文件。',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
bibliographyTitle: '参考资料与延伸阅读',
|
|
181
|
+
bibliography: bibliographyEntries,
|
|
182
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { saveFileEditor } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const SAVE_FILE_EDITOR_TOOL: ToolDefinition = {
|
|
7
|
+
entry: saveFileEditor,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
decodeSaveData,
|
|
4
|
+
encodeSaveData,
|
|
5
|
+
applyXor,
|
|
6
|
+
bytesToBase64,
|
|
7
|
+
base64ToBytes,
|
|
8
|
+
strToUtf8Bytes,
|
|
9
|
+
utf8BytesToStr,
|
|
10
|
+
detectFormat,
|
|
11
|
+
} from './logic';
|
|
12
|
+
|
|
13
|
+
describe('saveFileEditor logic', () => {
|
|
14
|
+
const sampleObj = { level: 10, coins: 500, player: 'Hero' };
|
|
15
|
+
const sampleJson = JSON.stringify(sampleObj, null, 2);
|
|
16
|
+
|
|
17
|
+
it('converts utf8 bytes back and forth', () => {
|
|
18
|
+
const bytes = strToUtf8Bytes('Hello World!');
|
|
19
|
+
expect(utf8BytesToStr(bytes)).toBe('Hello World!');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('converts base64 back and forth', () => {
|
|
23
|
+
const bytes = strToUtf8Bytes(sampleJson);
|
|
24
|
+
const b64 = bytesToBase64(bytes);
|
|
25
|
+
const resultBytes = base64ToBytes(b64);
|
|
26
|
+
expect(utf8BytesToStr(resultBytes)).toBe(sampleJson);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('applies XOR correctly', () => {
|
|
30
|
+
const key = 'secret123';
|
|
31
|
+
const bytes = strToUtf8Bytes('GameSaveData');
|
|
32
|
+
const obfuscated = applyXor(bytes, key);
|
|
33
|
+
const restored = applyXor(obfuscated, key);
|
|
34
|
+
expect(utf8BytesToStr(restored)).toBe('GameSaveData');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('decodes raw JSON save data', () => {
|
|
38
|
+
const res = decodeSaveData(sampleJson, { method: 'raw', xorKey: '' });
|
|
39
|
+
expect(res.success).toBe(true);
|
|
40
|
+
expect(res.data).toEqual(sampleObj);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('encodes and decodes Base64 save data', () => {
|
|
44
|
+
const b64 = encodeSaveData(sampleObj, { method: 'base64', xorKey: '' });
|
|
45
|
+
const decoded = decodeSaveData(b64, { method: 'base64', xorKey: '' });
|
|
46
|
+
expect(decoded.success).toBe(true);
|
|
47
|
+
expect(decoded.data).toEqual(sampleObj);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('encodes and decodes XOR + Base64 save data', () => {
|
|
51
|
+
const key = 'my_game_key';
|
|
52
|
+
const encoded = encodeSaveData(sampleObj, { method: 'xor', xorKey: key });
|
|
53
|
+
const decoded = decodeSaveData(encoded, { method: 'xor', xorKey: key });
|
|
54
|
+
expect(decoded.success).toBe(true);
|
|
55
|
+
expect(decoded.data).toEqual(sampleObj);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('auto-detects format', () => {
|
|
59
|
+
const rawFormat = detectFormat(sampleJson);
|
|
60
|
+
expect(rawFormat).toBe('raw');
|
|
61
|
+
|
|
62
|
+
const b64Str = encodeSaveData(sampleObj, { method: 'base64', xorKey: '' });
|
|
63
|
+
expect(detectFormat(b64Str)).toBe('base64');
|
|
64
|
+
|
|
65
|
+
const key = 'my_game_key';
|
|
66
|
+
const xorStr = encodeSaveData(sampleObj, { method: 'xor', xorKey: key });
|
|
67
|
+
expect(detectFormat(xorStr, key)).toBe('xor');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type EncryptionMethod = 'base64' | 'xor' | 'raw';
|
|
2
|
+
|
|
3
|
+
export interface ObfuscationOptions {
|
|
4
|
+
method: EncryptionMethod;
|
|
5
|
+
xorKey: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ParseResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
data: unknown | null;
|
|
11
|
+
error?: string;
|
|
12
|
+
detectedMethod?: EncryptionMethod;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function strToUtf8Bytes(str: string): Uint8Array {
|
|
16
|
+
return new TextEncoder().encode(str);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function utf8BytesToStr(bytes: Uint8Array): string {
|
|
20
|
+
return new TextDecoder().decode(bytes);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
24
|
+
let binary = '';
|
|
25
|
+
const len = bytes.byteLength;
|
|
26
|
+
for (let i = 0; i < len; i++) {
|
|
27
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
28
|
+
}
|
|
29
|
+
return btoa(binary);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function base64ToBytes(base64Str: string): Uint8Array {
|
|
33
|
+
const cleanStr = base64Str.trim().replace(/\s+/g, '');
|
|
34
|
+
const binary = atob(cleanStr);
|
|
35
|
+
const bytes = new Uint8Array(binary.length);
|
|
36
|
+
for (let i = 0; i < binary.length; i++) {
|
|
37
|
+
bytes[i] = binary.charCodeAt(i);
|
|
38
|
+
}
|
|
39
|
+
return bytes;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function applyXor(data: Uint8Array, keyStr: string): Uint8Array {
|
|
43
|
+
if (!keyStr) return data;
|
|
44
|
+
const keyBytes = strToUtf8Bytes(keyStr);
|
|
45
|
+
if (keyBytes.length === 0) return data;
|
|
46
|
+
|
|
47
|
+
const result = new Uint8Array(data.length);
|
|
48
|
+
for (let i = 0; i < data.length; i++) {
|
|
49
|
+
result[i] = data[i]! ^ keyBytes[i % keyBytes.length]!;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getDecodedString(trimmed: string, options: ObfuscationOptions): string {
|
|
55
|
+
if (options.method === 'raw') return trimmed;
|
|
56
|
+
const bytes = base64ToBytes(trimmed);
|
|
57
|
+
if (options.method === 'base64') return utf8BytesToStr(bytes);
|
|
58
|
+
if (options.method === 'xor') {
|
|
59
|
+
return utf8BytesToStr(applyXor(bytes, options.xorKey));
|
|
60
|
+
}
|
|
61
|
+
return trimmed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function decodeSaveData(rawContent: string, options: ObfuscationOptions): ParseResult {
|
|
65
|
+
const trimmed = rawContent.trim();
|
|
66
|
+
if (!trimmed) {
|
|
67
|
+
return { success: false, data: null, error: 'Empty file' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const decodedText = getDecodedString(trimmed, options);
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
data: JSON.parse(decodedText),
|
|
75
|
+
detectedMethod: options.method,
|
|
76
|
+
};
|
|
77
|
+
} catch (err: unknown) {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
data: null,
|
|
81
|
+
error: err instanceof Error ? err.message : String(err),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function encodeSaveData(data: unknown, options: ObfuscationOptions): string {
|
|
87
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
|
88
|
+
|
|
89
|
+
if (options.method === 'raw') {
|
|
90
|
+
return jsonStr;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const utf8Bytes = strToUtf8Bytes(jsonStr);
|
|
94
|
+
|
|
95
|
+
if (options.method === 'base64') {
|
|
96
|
+
return bytesToBase64(utf8Bytes);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.method === 'xor') {
|
|
100
|
+
const xorBytes = applyXor(utf8Bytes, options.xorKey);
|
|
101
|
+
return bytesToBase64(xorBytes);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return jsonStr;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function detectFormat(content: string, xorKey: string = ''): EncryptionMethod {
|
|
108
|
+
const trimmed = content.trim();
|
|
109
|
+
if (!trimmed) return 'raw';
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
JSON.parse(trimmed);
|
|
113
|
+
return 'raw';
|
|
114
|
+
} catch {
|
|
115
|
+
void 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (xorKey) {
|
|
119
|
+
try {
|
|
120
|
+
const bytes = base64ToBytes(trimmed);
|
|
121
|
+
const text = utf8BytesToStr(applyXor(bytes, xorKey));
|
|
122
|
+
JSON.parse(text);
|
|
123
|
+
return 'xor';
|
|
124
|
+
} catch {
|
|
125
|
+
void 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const bytes = base64ToBytes(trimmed);
|
|
131
|
+
const text = utf8BytesToStr(bytes);
|
|
132
|
+
JSON.parse(text);
|
|
133
|
+
return 'base64';
|
|
134
|
+
} catch {
|
|
135
|
+
void 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return 'raw';
|
|
139
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { saveFileEditor } from './entry';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const loader = saveFileEditor.i18n[locale] ?? saveFileEditor.i18n.en;
|
|
12
|
+
const content = loader ? await loader() : null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content && content.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface SaveFileEditorUI extends Record<string, string> {
|
|
2
|
+
title: string;
|
|
3
|
+
subtitle: string;
|
|
4
|
+
dropSaveFile: string;
|
|
5
|
+
orSelectFile: string;
|
|
6
|
+
encryptionMethod: string;
|
|
7
|
+
methodBase64: string;
|
|
8
|
+
methodXor: string;
|
|
9
|
+
methodRaw: string;
|
|
10
|
+
xorKeyLabel: string;
|
|
11
|
+
xorKeyPlaceholder: string;
|
|
12
|
+
jsonRawTitle: string;
|
|
13
|
+
encodeAndDownload: string;
|
|
14
|
+
copyEncoded: string;
|
|
15
|
+
copiedNotice: string;
|
|
16
|
+
decodedKeysCount: string;
|
|
17
|
+
dataSize: string;
|
|
18
|
+
detectedFormat: string;
|
|
19
|
+
exportPreviewLabel: string;
|
|
20
|
+
decodePanelTitle: string;
|
|
21
|
+
exportPanelTitle: string;
|
|
22
|
+
decodeError: string;
|
|
23
|
+
bytesUnit: string;
|
|
24
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -4,11 +4,13 @@ import { STEAM_CAPSULE_GENERATOR_TOOL } from './tool/steamCapsuleGenerator';
|
|
|
4
4
|
import { ITCHIO_GAME_TESTER_TOOL } from './tool/itchioGameTester';
|
|
5
5
|
import { SPRITE_SHEET_PACKER_TOOL } from './tool/spriteSheetPacker';
|
|
6
6
|
import { AUDIO_LOOP_POINT_FINDER_TOOL } from './tool/audioLoopPointFinder';
|
|
7
|
+
import { SAVE_FILE_EDITOR_TOOL } from './tool/saveFileEditor';
|
|
7
8
|
|
|
8
9
|
export const ALL_TOOLS: ToolDefinition[] = [
|
|
9
10
|
STEAM_CAPSULE_GENERATOR_TOOL,
|
|
10
11
|
ITCHIO_GAME_TESTER_TOOL,
|
|
11
12
|
SPRITE_SHEET_PACKER_TOOL,
|
|
12
13
|
AUDIO_LOOP_POINT_FINDER_TOOL,
|
|
14
|
+
SAVE_FILE_EDITOR_TOOL,
|
|
13
15
|
];
|
|
14
16
|
|