Skip to content

Commit 15c8936

Browse files
authored
Infinite craft save file handler.
1 parent 9d0153a commit 15c8936

1 file changed

Lines changed: 176 additions & 0 deletions

File tree

src/handlers/infiniteCraft.ts

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts";
2+
import CommonFormats, { Category } from "src/CommonFormats.ts";
3+
4+
const INFINITE_CRAFT = {
5+
name: "Infinite Craft Save File",
6+
format: "ic",
7+
extension: "ic",
8+
mime: "application/x-infinite-craft-ic",
9+
internal: "ic",
10+
category: Category.ARCHIVE,
11+
};
12+
13+
class txtToInfiniteCraftHandler implements FormatHandler {
14+
15+
public name: string = "txtToInfiniteCraft";
16+
public supportedFormats?: FileFormat[];
17+
public ready: boolean = false;
18+
19+
async init () {
20+
this.supportedFormats = [
21+
CommonFormats.TEXT.supported("text", true, false),
22+
{...INFINITE_CRAFT,
23+
from: false,
24+
to: true,
25+
lossless: false
26+
},
27+
];
28+
this.ready = true;
29+
}
30+
31+
async doConvert (
32+
inputFiles: FileData[],
33+
inputFormat: FileFormat,
34+
outputFormat: FileFormat
35+
): Promise<FileData[]> {
36+
const inputFile = inputFiles[0];
37+
const text = new TextDecoder().decode(inputFile.bytes);
38+
const words = text
39+
.split(/[^a-zA-Z0-9']+/)
40+
.filter(Boolean);
41+
42+
const emojis = ["💧", "🔥", "🌬️", "🌍", "⚡", "❄️", "🌟", "🌈", "🌊", "🍃"];
43+
44+
function getRandomEmoji(): string {
45+
return emojis[Math.floor(Math.random() * emojis.length)];
46+
}
47+
48+
const jsonData = {
49+
name: "Save 1",
50+
version: "1.0",
51+
created: Date.now(),
52+
updated: 0,
53+
instances: [] as any[],
54+
items: words.map((word, index) => ({
55+
id: index,
56+
text: word,
57+
emoji: getRandomEmoji(),
58+
})),
59+
};
60+
61+
const outputBytes = new TextEncoder().encode(JSON.stringify(jsonData, null, 2));
62+
63+
const cs = new CompressionStream("gzip");
64+
65+
const inputStream = new Response(outputBytes).body!;
66+
67+
const compressedStream = inputStream.pipeThrough(cs);
68+
69+
const compressedBytes = new Uint8Array(await new Response(compressedStream).arrayBuffer());
70+
71+
const inputFileName = inputFile.name;
72+
73+
const outputFileName = inputFileName.replace(/\.txt$/i, ".ic");
74+
75+
const outputFiles: FileData[] = [
76+
{
77+
name: outputFileName,
78+
bytes: compressedBytes,
79+
},
80+
];
81+
return outputFiles;
82+
}
83+
84+
}
85+
86+
class infiniteCraftToJsonHandler implements FormatHandler {
87+
88+
public name: string = "infiniteCraftToJson";
89+
public supportedFormats?: FileFormat[];
90+
public ready: boolean = false;
91+
92+
async init() {
93+
this.supportedFormats = [
94+
{...INFINITE_CRAFT,
95+
from: true,
96+
to: false,
97+
lossless: true
98+
},
99+
CommonFormats.JSON.supported("json", false, true, true),
100+
];
101+
this.ready = true;
102+
}
103+
104+
async doConvert (
105+
inputFiles: FileData[],
106+
inputFormat: FileFormat,
107+
outputFormat: FileFormat
108+
): Promise<FileData[]> {
109+
// Credit to als.ts
110+
if (inputFormat.internal !== "ic" || outputFormat.internal !== "json") {
111+
throw new TypeError(`Unsupported conversion path: ${inputFormat.internal} -> ${outputFormat.internal}`);
112+
}
113+
114+
const decoder = new TextDecoder("utf-8", { fatal: true });
115+
const encoder = new TextEncoder();
116+
117+
return Promise.all(inputFiles.map(async (inputFile) => {
118+
if (
119+
inputFile.bytes.length < 2
120+
|| inputFile.bytes[0] !== 0x1f
121+
|| inputFile.bytes[1] !== 0x8b
122+
) {
123+
throw new Error("Invalid IC file: expected gzip-compressed data.");
124+
}
125+
126+
const decompressedStream = new Blob([inputFile.bytes as BlobPart])
127+
.stream()
128+
.pipeThrough(new DecompressionStream("gzip"));
129+
const decompressedBytes = new Uint8Array(await new Response(decompressedStream).arrayBuffer());
130+
131+
let json: string;
132+
try {
133+
json = decoder.decode(decompressedBytes);
134+
} catch (_) {
135+
throw new Error("Invalid IC file: decompressed data is not UTF-8 JSON.");
136+
}
137+
if (json.trimStart().startsWith("[")) {
138+
throw new Error("Invalid IC file: decompressed data should not be an array in JSON.");
139+
} else if (!json.trimStart().startsWith("{")) {
140+
throw new Error("Invalid IC file: decompressed data is not JSON.");
141+
}
142+
143+
const baseNameParts = inputFile.name.split(".");
144+
const baseName = baseNameParts.length > 1
145+
? baseNameParts.slice(0, -1).join(".")
146+
: inputFile.name;
147+
148+
return {
149+
name: `${baseName}.json`,
150+
bytes: encoder.encode(json)
151+
};
152+
}));
153+
}
154+
155+
}
156+
157+
// What an IC file roughly looks like, for future reference:
158+
// (apart from the gzip encoding)
159+
//* {
160+
//* "name":"Save 1 (main)",
161+
//* "version":"1.0",
162+
//* "created": "<number>",
163+
//* "updated": "<number>",
164+
//* "instances": [
165+
//* {"itemId":89,"x":-5056,"y":2178},
166+
//* ...
167+
//* ],
168+
//* "items":[
169+
//* {"id":0,"text":"Water","emoji":"💧","recipes":[[1595,1]]},
170+
//* {"id":1,"text":"Fire","emoji":"🔥","recipes":[[24,25],[24,54],[246,54]]},
171+
//* ...
172+
//* id increments I think
173+
//* ]
174+
//* }
175+
176+
export {txtToInfiniteCraftHandler, infiniteCraftToJsonHandler};

0 commit comments

Comments
 (0)