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 - z A - Z 0 - 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 ( / \. t x t $ / 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