├── pages ├── ItemFull.css ├── info-table.css ├── ItemName.css ├── ItemSearch.css ├── ItemName.jsx ├── RecipeTable.css ├── index.css ├── SearchBox.css ├── index.jsx ├── ItemFull.jsx ├── App.jsx ├── RecipeTable.jsx ├── index.html ├── ItemSearch.jsx ├── db.js └── normalize.css ├── bstruct ├── package.json ├── index.mjs └── type-handler.mjs ├── .gitignore ├── package.json ├── src ├── export.mjs ├── data-typedef.mjs ├── pq.mjs ├── token-bucket.mjs ├── find-path.mjs ├── create-index.mjs └── index.mjs ├── README.md └── init.sql /pages/ItemFull.css: -------------------------------------------------------------------------------- 1 | .item-note { 2 | font-style: italic; 3 | } -------------------------------------------------------------------------------- /bstruct/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binary-struct", 3 | "type": "module", 4 | "main": "index.mjs", 5 | "dependencies": { 6 | "buffer": "^6.0.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | *.sqlite* 4 | *.db 5 | backups 6 | dist 7 | *.parcel-cache 8 | pages/data.json 9 | *.dat 10 | *.zst 11 | relevant_recipes.json 12 | -------------------------------------------------------------------------------- /pages/info-table.css: -------------------------------------------------------------------------------- 1 | 2 | .info-table, .info-table tr, .info-table td { 3 | border: none; 4 | padding: 0; 5 | outline: none; 6 | border-spacing: 0; 7 | } 8 | 9 | .info-table td { 10 | padding-right: 2em; 11 | } -------------------------------------------------------------------------------- /pages/ItemName.css: -------------------------------------------------------------------------------- 1 | .item-name, .item-link { 2 | font-style: normal; 3 | font-weight: normal; 4 | } 5 | 6 | .item-link { 7 | color: #0000ee; 8 | text-decoration: underline; 9 | } 10 | 11 | .item-link:hover { 12 | cursor: pointer; 13 | } -------------------------------------------------------------------------------- /pages/ItemSearch.css: -------------------------------------------------------------------------------- 1 | .search-item-title { 2 | margin-bottom: 0; 3 | } 4 | 5 | .search-item-subtitle { 6 | margin-top: 10px; 7 | } 8 | 9 | .search-item-info { 10 | font-style: italic; 11 | } 12 | 13 | .total-number-info { 14 | font-size: small; 15 | /* color: #666; */ 16 | } -------------------------------------------------------------------------------- /pages/ItemName.jsx: -------------------------------------------------------------------------------- 1 | import './ItemName.css'; 2 | 3 | export function ItemName({item}) { 4 | return {item.toString()} 5 | } 6 | 7 | export function ItemLink({item, onClick}) { 8 | const clickHandler = () => { 9 | onClick(item); 10 | }; 11 | 12 | return {item.toString()} 13 | } 14 | -------------------------------------------------------------------------------- /pages/RecipeTable.css: -------------------------------------------------------------------------------- 1 | .recipe-table-idx { 2 | text-align: right; 3 | } 4 | 5 | .recipe-table { 6 | outline: none; 7 | border-collapse: collapse; 8 | background-color: #f2f2f2; 9 | } 10 | 11 | .recipe-table td, .recipe-table tr, .recipe-table th { 12 | border: 1px solid rgb(160 160 160); 13 | padding: 8px 10px; 14 | } 15 | 16 | tbody > tr:nth-of-type(odd) { 17 | background-color: #ffffff; 18 | } 19 | 20 | .recipe-empty, .recipe-table-info { 21 | font-style: italic; 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infinite-autocraft", 3 | "version": "0.1.0", 4 | "description": "Auto crafter script for infinite-craft", 5 | "type": "module", 6 | "scripts": {}, 7 | "author": "szdytom ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@cloudpss/zstd": "^0.2.14", 11 | "better-sqlite3": "^9.4.1", 12 | "binary-struct": "file:./bstruct", 13 | "node-fetch": "^3.3.2", 14 | "playwright": "^1.41.2", 15 | "progress": "^2.0.3", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "parcel": "^2.11.0", 21 | "process": "^0.11.10" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/export.mjs: -------------------------------------------------------------------------------- 1 | import Database from 'better-sqlite3'; 2 | 3 | const db = new Database('./craft.sqlite', { readonly: true }); 4 | 5 | const get_items = db.prepare(` 6 | SELECT * FROM Items WHERE (mask & 1) = 0 ORDER BY dep ASC; 7 | `); 8 | 9 | const rows = get_items.all(); 10 | let elements = rows.map(r => { 11 | return { 12 | "text": r.handle, 13 | "emoji": r.emoji, 14 | "discovered": !!r.is_new, 15 | }; 16 | }); 17 | 18 | console.log(`localStorage.setItem('infinite-craft-data', ${JSON.stringify(JSON.stringify({ elements }))})`); 19 | 20 | process.on('exit', () => db.close()); 21 | process.on('SIGINT', () => process.exit(1)); 22 | process.on('SIGHUP', () => process.exit(1)); 23 | process.on('SIGTERM', () => process.exit(1)); 24 | -------------------------------------------------------------------------------- /pages/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | line-height: 1.3em; 3 | } 4 | 5 | main { 6 | width: 800px; 7 | margin: 20px auto; 8 | } 9 | 10 | footer { 11 | width: 800px; 12 | margin-top: 20px; 13 | margin-left: auto; 14 | margin-right: auto; 15 | margin-bottom: 10px; 16 | color: #a8a8a8; 17 | font-size: xx-small; 18 | } 19 | 20 | footer a { 21 | color: #a8a8a8; 22 | } 23 | 24 | footer .important-footer { 25 | color: black; 26 | } 27 | 28 | .full-width { 29 | width: 100%; 30 | } 31 | 32 | html { 33 | background-color: #ffffff; 34 | } 35 | 36 | a, footer .important-footer a { 37 | color: #0000ee; 38 | } 39 | 40 | .seo-info { 41 | overflow: hidden; 42 | width: 1px; 43 | height: 1px; 44 | } 45 | 46 | .a-button { 47 | margin-left: 5px; 48 | margin-right: 5px; 49 | text-decoration: underline; 50 | cursor: pointer; 51 | } 52 | -------------------------------------------------------------------------------- /pages/SearchBox.css: -------------------------------------------------------------------------------- 1 | .search-box-container { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .search-input { 7 | height: 40px; 8 | padding: 0 10px; 9 | border: 1px solid #000; 10 | border-radius: 0; 11 | min-width: 400px; 12 | } 13 | 14 | .search-button { 15 | height: 42px; 16 | padding: 0 15px; 17 | margin-left: 10px; 18 | background-color: #000; 19 | border: 1px solid black; 20 | color: #fff; 21 | cursor: pointer; 22 | } 23 | 24 | .lucky-button { 25 | height: 42px; 26 | padding: 0 15px; 27 | margin-left: 10px; 28 | border: 1px solid black; 29 | background-color: #fff; 30 | cursor: pointer; 31 | } 32 | 33 | .search-button:hover { 34 | background-color: #333; 35 | border: 1px solid #333; 36 | } 37 | 38 | .lucky-button:hover { 39 | border: 1px solid #333; 40 | background-color: #efefef; 41 | } 42 | 43 | .search-button:focus { 44 | outline: none; 45 | } -------------------------------------------------------------------------------- /src/data-typedef.mjs: -------------------------------------------------------------------------------- 1 | import { BASIC_TYPES } from 'binary-struct'; 2 | 3 | export class BinaryRecipe { 4 | static typedef = [ 5 | { field: 'id', type: BASIC_TYPES.u32 }, 6 | { field: 'ingrA_id', type: BASIC_TYPES.u16 }, 7 | { field: 'ingrB_id', type: BASIC_TYPES.u16 }, 8 | { field: 'result_id', type: BASIC_TYPES.u16 }, 9 | ]; 10 | } 11 | 12 | export class BinaryItem { 13 | static typedef = [ 14 | { field: 'id', type: BASIC_TYPES.u16 }, 15 | { field: 'handle', type: BASIC_TYPES.str }, 16 | { field: 'emoji', type: BASIC_TYPES.str }, 17 | { field: '_craft_path_source', type: BASIC_TYPES.u32 }, 18 | { field: 'dep', type: BASIC_TYPES.i16 }, 19 | ]; 20 | } 21 | 22 | export class BinaryTransferData { 23 | static typedef = [ 24 | { field: 'NOTHING_ID', type: BASIC_TYPES.u16 }, 25 | { field: 'items', type: BASIC_TYPES.array(BinaryItem) }, 26 | { field: 'recipes', type: BASIC_TYPES.array(BinaryRecipe) }, 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { App } from './App'; 3 | import { initialize } from './db'; 4 | 5 | async function main() { 6 | const container = document.getElementById('app'); 7 | 8 | container.innerHTML = '
Please wait a few seconds while we are downloading data...
'; 9 | try { 10 | await initialize(); 11 | } catch(e) { 12 | container.innerHTML = '
Failed to download data, please check your internet connection and reload the page.
'; 13 | throw e; 14 | } 15 | 16 | const root = createRoot(container); 17 | root.render(); 18 | } 19 | 20 | async function unregisterServiceWorker() { 21 | if ('serviceWorker' in navigator) { 22 | try { 23 | let res = await navigator.serviceWorker.getRegistration(); 24 | if (res != null) { 25 | res.unregister(); 26 | } 27 | } catch(e) { 28 | //... 29 | } 30 | } 31 | }; 32 | 33 | main(); 34 | unregisterServiceWorker(); 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infinite Autocraft 2 | 3 | This is a script to explore the game [Infinite Craft](https://neal.fun/infinite-craft/). 4 | 5 | ## Structure 6 | 7 | - `bstruct/`: My library to encode/decode JavaScript values into/from binary (shared between local and web app). 8 | - `pages/`: A react APP, the frontend of the explored result. 9 | - `src/index.mjs`: The main explore script, does crafting and saves result into SQLite database. 10 | - `src/pg.mjs` and `src/token-bucket.mjs`: My simple implementations of common data structures used (shared between local and web app). 11 | - `src/export.mjs`: Generated a line of code that can override data of the original Infinite Craft frontend (copy and execute the output in console). 12 | - `src/create-index.mjs`: Encode data for the frontend app. 13 | - `src/find-path.mjs`: Calculate crafting depth and ensures the database is correct. 14 | - `src/data-typedef.mjs`: defines the structure of binary data loaded by the frontend (shared between local and web app). -------------------------------------------------------------------------------- /bstruct/index.mjs: -------------------------------------------------------------------------------- 1 | import { BaseTypeHandler, CompoundTypeHandler } from './type-handler.mjs'; 2 | 3 | export { BASIC_TYPES } from './type-handler.mjs'; 4 | 5 | /** 6 | * Serializes JavaScript value to binary. 7 | * @param {any} value - value to serialize. 8 | * @param {BaseTypeHandler} type - type handler of the value. 9 | * @returns {ArrayBuffer} - the serialized binary buffer. 10 | */ 11 | export function serializeToBinary(value, type) { 12 | if (!(type instanceof BaseTypeHandler)) { 13 | type = new CompoundTypeHandler(type); 14 | } 15 | 16 | const res = new ArrayBuffer(type.sizeof(value)); 17 | const view = new DataView(res); 18 | type.serialize(view, 0, value); 19 | return res; 20 | } 21 | 22 | /** 23 | * Deserializes binary back to JavaScript value. 24 | * @param {DataView} view - buffer to deserialize. 25 | * @param {BaseTypeHandler} type - type handler of the desired value. 26 | * @returns {any} - the deserialized JavaScript value. 27 | */ 28 | export function deserializeFromBinary(view, type) { 29 | if (!(type instanceof BaseTypeHandler)) { 30 | type = new CompoundTypeHandler(type); 31 | } 32 | 33 | const tmp = type.deserialize(view, 0); 34 | return tmp.value; 35 | } 36 | -------------------------------------------------------------------------------- /pages/ItemFull.jsx: -------------------------------------------------------------------------------- 1 | import { RecipeTable } from './RecipeTable'; 2 | import './info-table.css'; 3 | import './ItemFull.css'; 4 | 5 | export default function ItemFull({ value }) { 6 | const note = value.note(); 7 | return ( 8 | <> 9 |

Basic Info of {value.handle}

10 | 11 | 12 | 13 | 14 | 15 | 16 |
Dictionary ID:{value.id}
Handle:{value.handle}
Emoji:{value.emoji}
Fundamental:{value.isFundamental() ? 'Yes' : 'No'}
Crafting Depth:{value.dep}
17 | { note != null &&

{value.note()}

} 18 |

Known Recipes That Craft {value.handle}

19 | 20 |

Known Recipes That Use {value.handle}

21 | 22 |

An Example Way Of Obtaining {value.handle}

23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/pq.mjs: -------------------------------------------------------------------------------- 1 | export class PriorityQueue { 2 | constructor() { 3 | this.val = [null]; 4 | } 5 | 6 | push(value, priority) { 7 | if (priority == null) { 8 | priority = value; 9 | } 10 | 11 | this.val.push([value, priority]); 12 | let id = this.val.length - 1; 13 | while (id > 1 && this.val[id][1] > this.val[id >>> 1][1]) { 14 | const kid = id >>> 1; 15 | [this.val[id], this.val[kid]] = [this.val[kid], this.val[id]]; 16 | id = kid; 17 | } 18 | return this; 19 | } 20 | 21 | pop() { 22 | let lv = this.val.pop(); 23 | if (this.val.length == 1) { return lv; } 24 | 25 | let res = this.val[1]; 26 | this.val[1] = lv; 27 | let id = 1; 28 | while (id * 2 < this.val.length) { 29 | if (this.val[id][1] > this.val[id * 2][1] && (id * 2 + 1 >= this.val.length || this.val[id][1] > this.val[id * 2 + 1][1])) { 30 | break; 31 | } 32 | 33 | let kid = (id * 2 + 1 >= this.val.length || this.val[id * 2][1] > this.val[id * 2 + 1][1]) ? id * 2 : id * 2 + 1; 34 | [this.val[id], this.val[kid]] = [this.val[kid], this.val[id]]; 35 | id = kid; 36 | } 37 | return res; 38 | } 39 | 40 | top() { 41 | return this.val[1]; 42 | } 43 | 44 | size() { 45 | return this.val.length - 1; 46 | } 47 | 48 | empty() { 49 | return this.size() == 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pages/App.jsx: -------------------------------------------------------------------------------- 1 | import './normalize.css'; 2 | import './index.css'; 3 | import ItemSearch from './ItemSearch'; 4 | 5 | export function App() { 6 | return ( 7 | <> 8 |
9 | 10 |
11 |
12 |
13 |

Contact: szdytom[at]qq.com. If you have suggestions, feedbacks or anything that else want to share with me, please feel free to send an email.

14 |

This website and related scripts is open-source on GitHub, please give me star if liked it.

15 |

This website is not owned or operated by neal.fun, nor does it represent the official opinions of neal.fun.

16 |

The content of the website is mostly collected and organized from the game Infinite Craft, and the crafting recipes are generated by AI. It may contain some discriminatory or offensive viewpoints, which do not represent the views supported by the website operator nor Github Pages. If you feel uncomfortable, please close this website.

17 |

This website does not use cookies.

18 |
19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/token-bucket.mjs: -------------------------------------------------------------------------------- 1 | export class Queue { 2 | constructor() { 3 | this.val = []; 4 | this.ptr = 0; 5 | } 6 | 7 | size() { 8 | return this.val.length - this.ptr; 9 | } 10 | 11 | get length() { 12 | return this.size(); 13 | } 14 | 15 | empty() { 16 | return this.size() == 0; 17 | } 18 | 19 | front() { 20 | return this.val[this.ptr]; 21 | } 22 | 23 | _rebuild() { 24 | if (this.ptr > 0) { 25 | this.val = this.val.slice(this.ptr); 26 | this.ptr = 0; 27 | } 28 | } 29 | 30 | popFront() { 31 | this.ptr += 1; 32 | if (this.ptr >= 16 && this.ptr >= this.val.length / 2) { 33 | this._rebuild(); 34 | } 35 | return this; 36 | } 37 | 38 | pushBack(x) { 39 | this.val.push(x); 40 | return this; 41 | } 42 | }; 43 | 44 | export class AsyncTokenBucket { 45 | constructor(limit) { 46 | this.wanted_by = new Queue(); 47 | this.limit = limit; 48 | this.tokens_used = 0; 49 | } 50 | 51 | aquire() { 52 | if (this.tokens_used < this.limit) { 53 | this.tokens_used += 1; 54 | return Promise.resolve(); 55 | } 56 | 57 | return new Promise((resolve, _reject) => this.wanted_by.pushBack(resolve)); 58 | } 59 | 60 | refill() { 61 | if (this.tokens_used > 0) { 62 | this.tokens_used -= 1; 63 | } 64 | 65 | if (this.tokens_used < this.limit && !this.wanted_by.empty()) { 66 | this.tokens_used += 1; 67 | const resolve = this.wanted_by.front(); 68 | this.wanted_by.popFront(); 69 | resolve(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS Items; 2 | DROP TABLE IF EXISTS Recipes; 3 | DROP TABLE IF EXISTS EnglishWords; 4 | 5 | CREATE TABLE Items ( 6 | id INTEGER PRIMARY KEY AUTOINCREMENT, 7 | handle TEXT NOT NULL 8 | UNIQUE, 9 | emoji TEXT DEFAULT NULL, 10 | is_new INTEGER NOT NULL 11 | DEFAULT 0, 12 | explore INTEGER NOT NULL 13 | DEFAULT 0, 14 | reward INTEGER NOT NULL 15 | DEFAULT 0, 16 | mask INTEGER NOT NULL 17 | DEFAULT (0), 18 | dep INTEGER, 19 | freq INTEGER 20 | ); 21 | 22 | CREATE UNIQUE INDEX idx_items_handle ON Items(handle); 23 | 24 | INSERT INTO Items (handle, emoji) VALUES ('Water', char(128167)); 25 | INSERT INTO Items (handle, emoji) VALUES ('Fire', char(128293)); 26 | INSERT INTO Items (handle, emoji) VALUES ('Wind', char(127788)); 27 | INSERT INTO Items (handle, emoji) VALUES ('Earth', char(127757)); 28 | INSERT INTO Items (handle, emoji, mask) VALUES ('Nothing', '', 1); 29 | 30 | CREATE TABLE Recipes ( 31 | id INTEGER PRIMARY KEY AUTOINCREMENT, 32 | ingrA_id INTEGER NOT NULL, 33 | ingrB_id INTEGER NOT NULL, 34 | result_id INTEGER DEFAULT NULL, 35 | mask INTEGER NOT NULL 36 | DEFAULT (0), 37 | FOREIGN KEY (ingrA_id) REFERENCES Items(id), 38 | FOREIGN KEY (ingrB_id) REFERENCES Items(id), 39 | FOREIGN KEY (result_id) REFERENCES Items(id) 40 | ); 41 | 42 | CREATE INDEX idx_recipes_ingrA ON Recipes(ingrA_id); 43 | CREATE INDEX idx_recipes_ingrB ON Recipes(ingrB_id); 44 | CREATE INDEX idx_recipes_res ON Recipes(result_id); 45 | 46 | CREATE TABLE EnglishWords ( 47 | id INTEGER PRIMARY KEY AUTOINCREMENT, 48 | lemma TEXT NOT NULL, 49 | PoS TEXT NOT NULL, 50 | freq INTEGER 51 | ); 52 | 53 | -------------------------------------------------------------------------------- /pages/RecipeTable.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { ItemName } from './ItemName'; 3 | import './RecipeTable.css'; 4 | 5 | export function RecipeTable({recipes, pageLimit = Infinity, indexed = false, emptyInfo = 'No recipes are known.'}) { 6 | if (recipes.length == 0) { 7 | return

{emptyInfo}

; 8 | } 9 | 10 | const [displayLimit, setDisplayLimit] = useState(pageLimit); 11 | 12 | const handleShowMore = () => { 13 | setDisplayLimit(displayLimit + pageLimit); 14 | }; 15 | 16 | const handleShowLess = () => { 17 | setDisplayLimit(pageLimit); 18 | }; 19 | 20 | const handleShowAll = () => { 21 | setDisplayLimit(recipes.length); 22 | }; 23 | 24 | 25 | return ( 26 | 27 | 28 | 29 | { indexed && } 30 | 31 | 32 | 33 | 34 | 35 | {recipes.slice(0, displayLimit).map(({ id, ingrA, ingrB, result }, index) => ( 36 | 37 | { indexed && } 38 | 39 | 40 | 41 | 42 | ))} 43 | {pageLimit < recipes.length && ( 44 | 45 | 51 | 52 | )} 53 |
{indexed}First IngredientSecond IngredientCraft Result
{index + 1}.
46 | {displayLimit < recipes.length && ...{recipes.length - displayLimit} rows omitted.} 47 | {displayLimit + pageLimit < recipes.length && Show More} 48 | {displayLimit < recipes.length && Show All{recipes.length > 1000 && '(Slow)'}} 49 | {displayLimit > pageLimit && Show Less} 50 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Infinite Craft Dictionary 9 | 10 | 11 |
12 |

Please enable JavaScript and wait for the App to load...

13 |
14 |
15 |

Infinite Craft Dictionary

16 |

Search Elements And Their Recipes In The Largest Dictionary of The Game Infinite Craft With Thousand Of Entries.

17 |

Get the fastest path to craft any item in Infinite Craft

18 |

This website can is a solver for Infinite Craft.

19 |

Infinite Craft Dictionary explores available recipes in the game Infinite Craft and calculates the fastest way to craft an item.
Simply choose an element in the search bar to get started

20 |

This website is a dictionary/wiki/recipe book of crafting in the game Infinite Craft.

21 |
22 |
23 |

Contact: szdytom[at]qq.com. If you have suggestions, feedbacks or anything that else want to share with me, please feel free to send an email.

24 |

This website and related scripts is open-source on GitHub, please give me star if liked it.

25 |

This website is not owned or operated by neal.fun, nor does it represent the official opinions of neal.fun.

26 |

The content of the website is mostly collected and organized from the game Infinite Craft, and the crafting recipes are generated by AI. It may contain some discriminatory or offensive viewpoints, which do not represent the views supported by the website operator nor Github Pages. If you feel uncomfortable, please close this website.

27 |

This website does not use cookies.

28 |
29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/find-path.mjs: -------------------------------------------------------------------------------- 1 | import Database from 'better-sqlite3'; 2 | import { PriorityQueue } from './pq.mjs'; 3 | import ProgressBar from 'progress'; 4 | 5 | const db = new Database('./craft.sqlite'); 6 | 7 | process.on('exit', () => db.close()); 8 | process.on('SIGINT', () => process.exit(1)); 9 | process.on('SIGHUP', () => process.exit(1)); 10 | process.on('SIGTERM', () => process.exit(1)); 11 | 12 | db.prepare('UPDATE Items SET dep = NULL WHERE dep IS NOT NULL').run(); 13 | 14 | const NOTHING_ID = db.prepare('SELECT id FROM Items WHERE handle=?').get('Nothing').id; 15 | 16 | const update_item_dep = db.prepare(` 17 | UPDATE Items SET dep = $dep WHERE id = $id 18 | `); 19 | 20 | const nothing_check = db.prepare(` 21 | SELECT COUNT(*) AS res FROM Recipes WHERE ingrA_id = $n OR ingrB_id = $n; 22 | `); 23 | 24 | if (nothing_check.get({n: NOTHING_ID}).res > 0) { 25 | console.log('WARN! Nothing used in recipe!!!'); 26 | } 27 | 28 | const all_items = db.prepare('SELECT id, handle, emoji FROM Items').all(); 29 | const all_recipes = db.prepare('SELECT id, ingrA_id, ingrB_id, result_id FROM Recipes WHERE result_id IS NOT NULL').all(); 30 | 31 | class Recipe { 32 | constructor(id, ingrA, ingrB, result) { 33 | this.id = id; 34 | this.ingrA = ingrA; 35 | this.ingrB = ingrB; 36 | this.result = result; 37 | } 38 | } 39 | 40 | class Item { 41 | constructor(id, handle, emoji) { 42 | this.id = id; 43 | this.handle = handle; 44 | this.emoji = emoji; 45 | this.can_craft = []; 46 | this.craft_by = []; 47 | this.craft_path_source = null; 48 | this.dep = -1; 49 | } 50 | 51 | addCanCraftRecipe(r) { 52 | this.can_craft.push(r); 53 | } 54 | 55 | addCraftByRecipe(r) { 56 | this.craft_by.push(r); 57 | } 58 | } 59 | 60 | const item_id_list = [], recipe_id_list = []; 61 | const items_by_id = {}, recipes_by_id = {}; 62 | 63 | for (const item of all_items) { 64 | item_id_list.push(item.id); 65 | let it = new Item(item.id, item.handle, item.emoji); 66 | items_by_id[item.id] = it; 67 | } 68 | 69 | for (const recipe of all_recipes) { 70 | if (recipe.result_id == NOTHING_ID) { 71 | continue; 72 | } 73 | 74 | if (recipe.ingrA == recipe.result_id || recipe.ingrB == recipe.result_id) { 75 | continue; 76 | } 77 | 78 | recipe_id_list.push(recipe.id); 79 | const ingrA = items_by_id[recipe.ingrA_id]; 80 | const ingrB = items_by_id[recipe.ingrB_id]; 81 | const result = items_by_id[recipe.result_id]; 82 | const r = new Recipe(recipe.id, ingrA, ingrB, result); 83 | recipes_by_id[recipe.id] = r; 84 | ingrA.addCanCraftRecipe(r); 85 | if (ingrB != ingrA) { 86 | ingrB.addCanCraftRecipe(r); 87 | } 88 | result.addCraftByRecipe(r); 89 | } 90 | 91 | let q = new PriorityQueue(); 92 | for (let i = 1; i <= 4; ++i) { 93 | items_by_id[i].dep = 0; 94 | q.push(i, 0); 95 | } 96 | 97 | let vis = []; 98 | items_by_id[NOTHING_ID].dep = 0; 99 | 100 | let bar = new ProgressBar(':bar [:percent :current/:total] [:rate items/s] [:etas]', { total: item_id_list.length }); 101 | 102 | while (!q.empty()) { 103 | let xid = q.pop()[0]; 104 | let x = items_by_id[xid]; 105 | if (vis[xid]) { 106 | continue; 107 | } 108 | vis[xid] = true; 109 | bar.tick(); 110 | 111 | for (const edge of x.can_craft) { 112 | const p = edge.ingrA.id == xid ? edge.ingrB : edge.ingrA; 113 | if (p.dep == -1 || p.id == NOTHING_ID) { 114 | continue; 115 | } 116 | 117 | const y = edge.result; 118 | const z = Math.max(p.dep, x.dep) + 1; 119 | if (y.dep == -1 || y.dep > z) { 120 | y.dep = z; 121 | y.craft_path_source = edge; 122 | q.push(y.id, -z); 123 | } 124 | } 125 | } 126 | 127 | bar.update(1); 128 | bar.terminate(); 129 | 130 | bar = new ProgressBar(':bar [:percent :current/:total] [:rate items/s] [:etas]', { total: item_id_list.length }); 131 | 132 | db.transaction(() => { 133 | for (const id of item_id_list) { 134 | if (id == NOTHING_ID) { 135 | continue; 136 | } 137 | const it = items_by_id[id]; 138 | if (it.dep != -1) { 139 | update_item_dep.run({ id, dep: it.dep }); 140 | bar.tick(); 141 | } 142 | } 143 | })(); 144 | 145 | bar.update(1); 146 | bar.terminate(); 147 | -------------------------------------------------------------------------------- /src/create-index.mjs: -------------------------------------------------------------------------------- 1 | import Database from 'better-sqlite3'; 2 | import { PriorityQueue } from './pq.mjs'; 3 | import fs from 'node:fs/promises'; 4 | import { serializeToBinary } from 'binary-struct'; 5 | import { BinaryItem, BinaryRecipe, BinaryTransferData } from './data-typedef.mjs'; 6 | import ProgressBar from 'progress'; 7 | 8 | const db = new Database('./craft.sqlite', { readonly: true }); 9 | 10 | const all_items = db.prepare('SELECT id, handle, emoji FROM Items').all(); 11 | const all_recipes = db.prepare('SELECT id, ingrA_id, ingrB_id, result_id FROM Recipes WHERE result_id IS NOT NULL').all(); 12 | 13 | class Recipe { 14 | constructor(id, ingrA, ingrB, result) { 15 | this.id = id; 16 | this.ingrA = ingrA; 17 | this.ingrB = ingrB; 18 | this.result = result; 19 | } 20 | 21 | toString() { 22 | return `${this.ingrA} + ${this.ingrB} = ${this.result}`; 23 | } 24 | } 25 | 26 | class Item { 27 | constructor(id, handle, emoji) { 28 | this.id = id; 29 | this.handle = handle; 30 | this.emoji = emoji; 31 | this.can_craft = []; 32 | this.craft_by = []; 33 | this.craft_path_source = null; 34 | this.dep = -1; 35 | } 36 | 37 | addCanCraftRecipe(r) { 38 | this.can_craft.push(r); 39 | } 40 | 41 | addCraftByRecipe(r) { 42 | this.craft_by.push(r); 43 | } 44 | 45 | isNothing() { 46 | return this.handle == 'Nothing'; 47 | } 48 | 49 | toString() { 50 | return this.handle; 51 | } 52 | } 53 | 54 | const item_id_list = [], recipe_id_list = []; 55 | const items_by_id = {}, recipes_by_id = {}; 56 | 57 | let NOTHING_ID = null; 58 | for (const item of all_items) { 59 | item_id_list.push(item.id); 60 | let it = new Item(item.id, item.handle, item.emoji); 61 | items_by_id[item.id] = it; 62 | if (it.isNothing()) { 63 | NOTHING_ID = it.id; 64 | } 65 | } 66 | 67 | for (const recipe of all_recipes) { 68 | if (recipe.result_id == NOTHING_ID) { 69 | continue; 70 | } 71 | if (recipe.ingrA == recipe.result_id || recipe.ingrB == recipe.result_id) { 72 | continue; 73 | } 74 | 75 | recipe_id_list.push(recipe.id); 76 | const ingrA = items_by_id[recipe.ingrA_id]; 77 | const ingrB = items_by_id[recipe.ingrB_id]; 78 | const result = items_by_id[recipe.result_id]; 79 | const r = new Recipe(recipe.id, ingrA, ingrB, result); 80 | recipes_by_id[recipe.id] = r; 81 | ingrA.addCanCraftRecipe(r); 82 | if (ingrB != ingrA) { 83 | ingrB.addCanCraftRecipe(r); 84 | } 85 | result.addCraftByRecipe(r); 86 | } 87 | 88 | let q = new PriorityQueue(); 89 | for (let i = 1; i <= 4; ++i) { 90 | items_by_id[i].dep = 0; 91 | q.push(i, 0); 92 | } 93 | 94 | let vis = []; 95 | items_by_id[NOTHING_ID].dep = 0; 96 | 97 | 98 | const bar = new ProgressBar(':bar [:percent :current/:total] [:rate items/s] [:etas]', { total: item_id_list.length }); 99 | 100 | while (!q.empty()) { 101 | let xid = q.pop()[0]; 102 | let x = items_by_id[xid]; 103 | if (vis[xid]) { 104 | continue; 105 | } 106 | vis[xid] = true; 107 | bar.tick(); 108 | 109 | for (const edge of x.can_craft) { 110 | const p = edge.ingrA.id == xid ? edge.ingrB : edge.ingrA; 111 | if (p.dep == -1 || p.id == NOTHING_ID) { 112 | continue; 113 | } 114 | 115 | const y = edge.result; 116 | const z = Math.max(p.dep, x.dep) + 1; 117 | if (y.dep == -1 || y.dep > z) { 118 | y.dep = z; 119 | y.craft_path_source = edge; 120 | q.push(y.id, -z); 121 | } 122 | } 123 | } 124 | 125 | bar.update(1); 126 | bar.terminate(); 127 | 128 | let res = new BinaryTransferData(); 129 | res.NOTHING_ID = NOTHING_ID; 130 | res.items = []; 131 | res.recipes = []; 132 | for (const id of item_id_list) { 133 | const it = items_by_id[id]; 134 | let b = new BinaryItem(); 135 | b.id = it.id; 136 | b.handle = it.handle; 137 | b.emoji = it.emoji; 138 | b.dep = it.dep; 139 | b._craft_path_source = it.craft_path_source?.id ?? 0, 140 | res.items.push(b); 141 | } 142 | 143 | 144 | for (const id of recipe_id_list) { 145 | const recipe = recipes_by_id[id]; 146 | let b = new BinaryRecipe(); 147 | b.id = recipe.id; 148 | b.ingrA_id = recipe.ingrA.id; 149 | b.ingrB_id = recipe.ingrB.id; 150 | b.result_id = recipe.result.id; 151 | res.recipes.push(b); 152 | } 153 | 154 | await fs.writeFile('./craft.dat', Buffer.from(serializeToBinary(res, BinaryTransferData))); 155 | 156 | process.on('exit', () => db.close()); 157 | -------------------------------------------------------------------------------- /pages/ItemSearch.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Item, Recipes } from './db'; 3 | import ItemFull from './ItemFull'; 4 | import { ItemLink } from './ItemName'; 5 | import { RecipeTable } from './RecipeTable'; 6 | import './ItemSearch.css'; 7 | import './SearchBox.css'; 8 | 9 | function RecipeSearchResult({ keyword, children }) { 10 | const val = keyword.split('+'); 11 | let res = []; 12 | for (let i = 1; i < val.length; i += 1) { 13 | const Ahandle = val.slice(0, i).join('+').trim(); 14 | const Bhandle = val.slice(i).join('+').trim(); 15 | const A = Item.loadByHandle(Ahandle); 16 | if (A == null) { 17 | continue; 18 | } 19 | 20 | const R = A.can_craft.filter(r => ( 21 | (r.ingrA.handle == Ahandle && r.ingrB.handle == Bhandle) || 22 | (r.ingrB.handle == Ahandle && r.ingrA.handle == Bhandle) 23 | )); 24 | res = res.concat(R); 25 | } 26 | if (res.length == 0) { 27 | return children; 28 | } 29 | return ( 30 |
31 |

Matching Recipe

32 | {[]} 33 |
34 | ); 35 | } 36 | 37 | function SearchResult({ keyword, onClick }) { 38 | if (keyword == null || keyword == '') { 39 | return ( 40 | <> 41 |

Type the name of the element you are interested in the search bar, and click "Search".

42 | 43 | ); 44 | } 45 | 46 | if (keyword.startsWith('?=')) { 47 | return ( 48 | 49 |

No match found.

50 |
51 | ); 52 | } 53 | 54 | const exact_match = Item.loadByHandle(keyword); 55 | if (exact_match != null) { 56 | return ( 57 | <> 58 | {keyword.includes('+') && } 59 | {[]} 60 | 61 | ); 62 | } 63 | 64 | const contain_match = Item.findByHandleContains(keyword.trim()); 65 | if (contain_match.length > 0) { 66 | const display_limit = 10; 67 | if (contain_match.length > display_limit) { 68 | const more = contain_match.length - display_limit; 69 | const displays = contain_match.slice(0, display_limit); 70 | return ( 71 |

72 | <>No exact match found, similar elements are 73 | {displays.map((x) => ( 74 | 75 | 76 | <>, 77 | 78 | ))} 79 | <>...and {more} more. 80 |

81 | ); 82 | } else if (contain_match.length == 1) { 83 | return ( 84 |

No exact match found, similar element is .

85 | ); 86 | } else { 87 | return ( 88 |

89 | <>No exact match found, similar elements are 90 | {contain_match.map((x, i) => ( 91 | 92 | {i > 0 && ', '} 93 | {i == contain_match.length - 1 && 'and '} 94 | 95 | 96 | ))} 97 | <>. 98 |

99 | ) 100 | } 101 | } 102 | 103 | if (keyword.includes('+')) { 104 | return ( 105 | 106 |

No match found.

107 |
108 | ); 109 | } 110 | 111 | return

No match found.

; 112 | } 113 | 114 | export default function ItemSearch() { 115 | const search_by_hash = decodeURIComponent(location.hash.slice(1)); 116 | const [searchKeyword, setSearchKeyword] = useState(search_by_hash); 117 | const [searchTerm, setSearchTerm] = useState(search_by_hash); 118 | 119 | useEffect(() => { 120 | const handler = () => { 121 | const search_by_hash = decodeURIComponent(location.hash.slice(1)); 122 | setSearchKeyword(search_by_hash); 123 | setSearchTerm(search_by_hash) 124 | }; 125 | 126 | window.addEventListener('hashchange', handler); 127 | return () => { 128 | window.removeEventListener('hashchange', handler); 129 | } 130 | }, [setSearchTerm, setSearchKeyword]); 131 | 132 | const makeSearch = (term) => { 133 | setSearchKeyword(term); 134 | window.location.hash = '#' + term; 135 | }; 136 | 137 | const handleSearch = () => { 138 | makeSearch(searchTerm); 139 | }; 140 | 141 | const handleLucky = () => { 142 | const keyword = Item.getRandomHandle(); 143 | setSearchTerm(keyword); 144 | makeSearch(keyword); 145 | }; 146 | 147 | const handleChange = (e) => { 148 | setSearchTerm(e.target.value); 149 | }; 150 | 151 | const handleLinkClick = (item) => { 152 | setSearchTerm(item.handle); 153 | makeSearch(item.handle); 154 | }; 155 | 156 | const handleInputKeydown = (event) => { 157 | if (event.code == 'Enter') { 158 | handleSearch(); 159 | } 160 | }; 161 | 162 | return ( 163 |
164 |

Search Elements

165 |

...and Their Recipes In The Largest Dictionary of The Game Infinite Craft.

166 |
167 | 175 | 178 | 181 |
182 | {Item.count} elements, {Recipes.count} recipes. 183 |
184 | 185 |
186 |
187 | ); 188 | }; -------------------------------------------------------------------------------- /pages/db.js: -------------------------------------------------------------------------------- 1 | import { Queue } from '../src/token-bucket.mjs'; 2 | import { BinaryTransferData } from '../src/data-typedef.mjs'; 3 | import { deserializeFromBinary } from 'binary-struct'; 4 | 5 | // Due to some strange problems 6 | // Comment this line if debugging with `parcel serve` 7 | import { decompress } from '@cloudpss/zstd'; 8 | 9 | async function downloadRawData() { 10 | if (process.env.NODE_ENV === 'development') { 11 | const response = await fetch(new URL('../craft.dat', import.meta.url)); 12 | const raw = await response.arrayBuffer(); 13 | return raw; 14 | } 15 | const response = await fetch(new URL('../craft.dat.zst', import.meta.url)); 16 | const raw = await response.arrayBuffer(); 17 | return decompress(raw).buffer; 18 | } 19 | 20 | let NOTHING_ID; 21 | const items_by_id = new Map(), recipes_by_id = new Map(); 22 | const items_index_by_handle = new Map(); 23 | const item_id_list = [], recipes_id_list = []; 24 | 25 | export async function initialize() { 26 | const raw_data = await downloadRawData(); 27 | const adata = deserializeFromBinary(new DataView(raw_data), BinaryTransferData); 28 | 29 | NOTHING_ID = adata.NOTHING_ID; 30 | for (const b of adata.items) { 31 | item_id_list.push(b.id); 32 | items_index_by_handle.set(b.handle, b.id); 33 | b._can_craft = []; 34 | b._craft_by = []; 35 | items_by_id.set(b.id, b); 36 | } 37 | 38 | for (const b of adata.recipes) { 39 | recipes_id_list.push(b.id); 40 | recipes_by_id.set(b.id, b); 41 | items_by_id.get(b.ingrA_id)._can_craft.push(b.id); 42 | if (b.ingrA_id != b.ingrB_id) { 43 | items_by_id.get(b.ingrB_id)._can_craft.push(b.id); 44 | } 45 | items_by_id.get(b.result_id)._craft_by.push(b.id); 46 | } 47 | 48 | Item.count = adata.items.length; 49 | Recipes.count = adata.recipes.length; 50 | } 51 | 52 | export class Recipes { 53 | constructor({id, ingrA, ingrB, result}) { 54 | this.id = id; 55 | this.ingrA = ingrA; 56 | this.ingrB = ingrB; 57 | this.result = result; 58 | } 59 | 60 | static recipes_loaded = new Map(); 61 | static count = 0; 62 | 63 | static loadById(id) { 64 | if (id == 0 || !recipes_by_id.has(id)) { 65 | return null; 66 | } 67 | 68 | if (this.recipes_loaded.has(id)) { 69 | return this.recipes_loaded.get(id); 70 | } 71 | 72 | const {ingrA_id, ingrB_id, result_id} = recipes_by_id.get(id); 73 | const res = new Recipes({ 74 | id, 75 | ingrA: Item.loadById(ingrA_id), 76 | ingrB: Item.loadById(ingrB_id), 77 | result: Item.loadById(result_id), 78 | }); 79 | this.recipes_loaded.set(id, res); 80 | return res; 81 | } 82 | } 83 | 84 | export class Item { 85 | constructor({id, handle, emoji, _can_craft, _craft_by, dep, _craft_path_source}) { 86 | this.id = id; 87 | this.handle = handle; 88 | this.emoji = emoji; 89 | this._can_craft = _can_craft; 90 | this._craft_by = _craft_by; 91 | this._craft_path_source = _craft_path_source; 92 | this.dep = dep; 93 | } 94 | 95 | get can_craft() { 96 | return this._can_craft.map(x => Recipes.loadById(x)); 97 | } 98 | 99 | get craft_by() { 100 | return this._craft_by.map(x => Recipes.loadById(x)); 101 | } 102 | 103 | get craft_path_source() { 104 | return Recipes.loadById(this._craft_path_source); 105 | } 106 | 107 | note() { 108 | if (this.id == NOTHING_ID) { 109 | return 'This element is only used to indicate the result of elements that don\'t craft, you cannot actually craft this element in game.'; 110 | } 111 | return null; 112 | } 113 | 114 | isFundamental() { 115 | return this.dep == 0; 116 | } 117 | 118 | calcPath() { 119 | if (this.isFundamental()) { 120 | return []; 121 | } 122 | 123 | let q = new Queue(); 124 | let vis = new Set(), res = []; 125 | q.pushBack(this.craft_path_source); 126 | while (q.size()) { 127 | let x = q.front(); 128 | q.popFront(); 129 | if (vis.has(x.id)) { 130 | continue; 131 | } 132 | vis.add(x.id); 133 | res.push([x.id, x.result.dep]); 134 | if (x.ingrA.dep != 0) { 135 | q.pushBack(x.ingrA.craft_path_source); 136 | } 137 | 138 | if (x.ingrB.dep != 0) { 139 | q.pushBack(x.ingrB.craft_path_source); 140 | } 141 | } 142 | 143 | res.sort((a, b) => { 144 | if (a[1] < b[1]) { return -1; } 145 | if (a[1] > b[1]) { return 1; } 146 | return 0; 147 | }); 148 | let ans = []; 149 | for (const id of res) { 150 | ans.push(Recipes.loadById(id[0])); 151 | } 152 | return ans; 153 | } 154 | 155 | toString() { 156 | return this.handle; 157 | } 158 | 159 | static count = 0; 160 | 161 | static items_loaded = new Map(); 162 | 163 | static loadById(id) { 164 | if (this.items_loaded.has(id)) { 165 | this.items_loaded.get(id); 166 | } 167 | 168 | const {handle, emoji, _can_craft, _craft_by, dep, _craft_path_source} = items_by_id.get(id); 169 | const res = new Item({ 170 | id, handle, emoji, dep, 171 | _can_craft, 172 | _craft_by, 173 | _craft_path_source, 174 | }); 175 | this.items_loaded.set(id, res); 176 | return res; 177 | } 178 | 179 | static loadByHandle(handle) { 180 | const id = items_index_by_handle.get(handle); 181 | if (id == null) { 182 | return null; 183 | } 184 | 185 | return this.loadById(id); 186 | } 187 | 188 | static getRandomHandle() { 189 | const v = Math.floor(Math.random() * this.count); 190 | const id = item_id_list[v]; 191 | return items_by_id.get(id).handle; 192 | } 193 | 194 | static findByHandleContains(keyword) { 195 | keyword = keyword.toLowerCase(); 196 | let matches = []; 197 | for (let id of item_id_list) { 198 | const row = items_by_id.get(id); 199 | const handle = row.handle.toLowerCase(); 200 | if (handle.includes(keyword)) { 201 | matches.push(row); 202 | } 203 | } 204 | 205 | matches.sort((a, b) => { 206 | if (a.handle.length < b.handle.length) { return -1; } 207 | if (a.handle.length > b.handle.length) { return 1; } 208 | return 0; 209 | }); 210 | return matches.map(r => Item.loadById(r.id)); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /pages/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; 13 | /* 1 */ 14 | -webkit-text-size-adjust: 100%; 15 | /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers. 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Render the `main` element consistently in IE. 31 | */ 32 | 33 | main { 34 | display: block; 35 | } 36 | 37 | /** 38 | * Correct the font size and margin on `h1` elements within `section` and 39 | * `article` contexts in Chrome, Firefox, and Safari. 40 | */ 41 | 42 | h1 { 43 | font-size: 2em; 44 | margin: 0.67em 0; 45 | } 46 | 47 | /* Grouping content 48 | ========================================================================== */ 49 | 50 | /** 51 | * 1. Add the correct box sizing in Firefox. 52 | * 2. Show the overflow in Edge and IE. 53 | */ 54 | 55 | hr { 56 | box-sizing: content-box; 57 | /* 1 */ 58 | height: 0; 59 | /* 1 */ 60 | overflow: visible; 61 | /* 2 */ 62 | } 63 | 64 | /** 65 | * 1. Correct the inheritance and scaling of font size in all browsers. 66 | * 2. Correct the odd `em` font sizing in all browsers. 67 | */ 68 | 69 | pre { 70 | font-family: monospace, monospace; 71 | /* 1 */ 72 | font-size: 1em; 73 | /* 2 */ 74 | } 75 | 76 | /* Text-level semantics 77 | ========================================================================== */ 78 | 79 | /** 80 | * Remove the gray background on active links in IE 10. 81 | */ 82 | 83 | a { 84 | background-color: transparent; 85 | } 86 | 87 | /** 88 | * 1. Remove the bottom border in Chrome 57- 89 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 90 | */ 91 | 92 | abbr[title] { 93 | border-bottom: none; 94 | /* 1 */ 95 | text-decoration: underline; 96 | /* 2 */ 97 | text-decoration: underline dotted; 98 | /* 2 */ 99 | } 100 | 101 | /** 102 | * Add the correct font weight in Chrome, Edge, and Safari. 103 | */ 104 | 105 | b, 106 | strong { 107 | font-weight: bolder; 108 | } 109 | 110 | /** 111 | * 1. Correct the inheritance and scaling of font size in all browsers. 112 | * 2. Correct the odd `em` font sizing in all browsers. 113 | */ 114 | 115 | code, 116 | kbd, 117 | samp { 118 | font-family: monospace, monospace; 119 | /* 1 */ 120 | font-size: 1em; 121 | /* 2 */ 122 | } 123 | 124 | /** 125 | * Add the correct font size in all browsers. 126 | */ 127 | 128 | small { 129 | font-size: 80%; 130 | } 131 | 132 | /** 133 | * Prevent `sub` and `sup` elements from affecting the line height in 134 | * all browsers. 135 | */ 136 | 137 | sub, 138 | sup { 139 | font-size: 75%; 140 | line-height: 0; 141 | position: relative; 142 | vertical-align: baseline; 143 | } 144 | 145 | sub { 146 | bottom: -0.25em; 147 | } 148 | 149 | sup { 150 | top: -0.5em; 151 | } 152 | 153 | /* Embedded content 154 | ========================================================================== */ 155 | 156 | /** 157 | * Remove the border on images inside links in IE 10. 158 | */ 159 | 160 | img { 161 | border-style: none; 162 | } 163 | 164 | /* Forms 165 | ========================================================================== */ 166 | 167 | /** 168 | * 1. Change the font styles in all browsers. 169 | * 2. Remove the margin in Firefox and Safari. 170 | */ 171 | 172 | button, 173 | input, 174 | optgroup, 175 | select, 176 | textarea { 177 | font-family: inherit; 178 | /* 1 */ 179 | font-size: 100%; 180 | /* 1 */ 181 | line-height: 1.15; 182 | /* 1 */ 183 | margin: 0; 184 | /* 2 */ 185 | } 186 | 187 | /** 188 | * Show the overflow in IE. 189 | * 1. Show the overflow in Edge. 190 | */ 191 | 192 | button, 193 | input { 194 | /* 1 */ 195 | overflow: visible; 196 | } 197 | 198 | /** 199 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 200 | * 1. Remove the inheritance of text transform in Firefox. 201 | */ 202 | 203 | button, 204 | select { 205 | /* 1 */ 206 | text-transform: none; 207 | } 208 | 209 | /** 210 | * Correct the inability to style clickable types in iOS and Safari. 211 | */ 212 | 213 | button, 214 | [type="button"], 215 | [type="reset"], 216 | [type="submit"] { 217 | -webkit-appearance: button; 218 | } 219 | 220 | /** 221 | * Remove the inner border and padding in Firefox. 222 | */ 223 | 224 | button::-moz-focus-inner, 225 | [type="button"]::-moz-focus-inner, 226 | [type="reset"]::-moz-focus-inner, 227 | [type="submit"]::-moz-focus-inner { 228 | border-style: none; 229 | padding: 0; 230 | } 231 | 232 | /** 233 | * Restore the focus styles unset by the previous rule. 234 | */ 235 | 236 | * { 237 | outline: none; 238 | } 239 | 240 | th { 241 | text-align: left; 242 | } 243 | 244 | /** 245 | * Correct the padding in Firefox. 246 | */ 247 | 248 | fieldset { 249 | padding: 0.35em 0.75em 0.625em; 250 | } 251 | 252 | /** 253 | * 1. Correct the text wrapping in Edge and IE. 254 | * 2. Correct the color inheritance from `fieldset` elements in IE. 255 | * 3. Remove the padding so developers are not caught out when they zero out 256 | * `fieldset` elements in all browsers. 257 | */ 258 | 259 | legend { 260 | box-sizing: border-box; 261 | /* 1 */ 262 | color: inherit; 263 | /* 2 */ 264 | display: table; 265 | /* 1 */ 266 | max-width: 100%; 267 | /* 1 */ 268 | padding: 0; 269 | /* 3 */ 270 | white-space: normal; 271 | /* 1 */ 272 | } 273 | 274 | /** 275 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 276 | */ 277 | 278 | progress { 279 | vertical-align: baseline; 280 | } 281 | 282 | /** 283 | * Remove the default vertical scrollbar in IE 10+. 284 | */ 285 | 286 | textarea { 287 | overflow: auto; 288 | } 289 | 290 | /** 291 | * 1. Add the correct box sizing in IE 10. 292 | * 2. Remove the padding in IE 10. 293 | */ 294 | 295 | [type="checkbox"], 296 | [type="radio"] { 297 | box-sizing: border-box; 298 | /* 1 */ 299 | padding: 0; 300 | /* 2 */ 301 | } 302 | 303 | /** 304 | * Correct the cursor style of increment and decrement buttons in Chrome. 305 | */ 306 | 307 | [type="number"]::-webkit-inner-spin-button, 308 | [type="number"]::-webkit-outer-spin-button { 309 | height: auto; 310 | } 311 | 312 | /** 313 | * 1. Correct the odd appearance in Chrome and Safari. 314 | * 2. Correct the outline style in Safari. 315 | */ 316 | 317 | [type="search"] { 318 | -webkit-appearance: textfield; 319 | /* 1 */ 320 | outline-offset: -2px; 321 | /* 2 */ 322 | } 323 | 324 | /** 325 | * Remove the inner padding in Chrome and Safari on macOS. 326 | */ 327 | 328 | [type="search"]::-webkit-search-decoration { 329 | -webkit-appearance: none; 330 | } 331 | 332 | /** 333 | * 1. Correct the inability to style clickable types in iOS and Safari. 334 | * 2. Change font properties to `inherit` in Safari. 335 | */ 336 | 337 | ::-webkit-file-upload-button { 338 | -webkit-appearance: button; 339 | /* 1 */ 340 | font: inherit; 341 | /* 2 */ 342 | } 343 | 344 | /* Interactive 345 | ========================================================================== */ 346 | 347 | /* 348 | * Add the correct display in Edge, IE 10+, and Firefox. 349 | */ 350 | 351 | details { 352 | display: block; 353 | } 354 | 355 | /* 356 | * Add the correct display in all browsers. 357 | */ 358 | 359 | summary { 360 | display: list-item; 361 | } 362 | 363 | /* Misc 364 | ========================================================================== */ 365 | 366 | /** 367 | * Add the correct display in IE 10+. 368 | */ 369 | 370 | template { 371 | display: none; 372 | } 373 | 374 | /** 375 | * Add the correct display in IE 10. 376 | */ 377 | 378 | [hidden] { 379 | display: none; 380 | } -------------------------------------------------------------------------------- /src/index.mjs: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import Database from 'better-sqlite3'; 3 | import { AsyncTokenBucket, Queue } from './token-bucket.mjs'; 4 | import fs from 'node:fs/promises'; 5 | import { firefox } from 'playwright'; 6 | 7 | const CLASSIC_UA = 'Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0'; 8 | const API_ENDPOINT = 'https://neal.fun/api/infinite-craft/pair?'; 9 | const DISGUISE_HEADERS = { 10 | // 'User-Agent': CLASSIC_UA, 11 | 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.7,zh-TW;q=0.5,zh-HK;q=0.3,en;q=0.2', 12 | // 'Referer': 'https://neal.fun/infinite-craft/', 13 | 'DNT': '1', 14 | 'Sec-Fetch-Dest': 'empty', 15 | 'Sec-Fetch-Mode': 'cors', 16 | 'Sec-Fetch-Site': 'same-origin', 17 | 'Sec-GPC': '1', 18 | 'Connection': 'keep-alive', 19 | }; 20 | 21 | const RecipeDB_API_ENDPOINT = 'https://infini-recipe.fly.dev/recipes?page='; 22 | const RecipeDB_DISGUISE_HEADERS = { 23 | 'User-Agent': CLASSIC_UA, 24 | 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.7,zh-TW;q=0.5,zh-HK;q=0.3,en;q=0.2', 25 | 'Referer': 'https://infini-recipe.vercel.app/', 26 | 'Origin': 'https://infini-recipe.vercel.app', 27 | 'Sec-Fetch-Dest': 'empty', 28 | 'Sec-Fetch-Mode': 'cors', 29 | 'Sec-Fetch-Site': 'cross-site', 30 | // 'Connection': 'keep-alive', 31 | 'TE': 'trailers', 32 | } 33 | 34 | let isExiting = false; 35 | 36 | function sleep(t) { 37 | return new Promise((res) => { 38 | setTimeout(res, t); 39 | }); 40 | } 41 | 42 | const browser = await firefox.launch({ 43 | // headless: false, 44 | proxy: { 45 | server: 'sock5://127.0.0.1:2080', 46 | }, 47 | }); 48 | const page = await browser.newPage(); 49 | await page.goto('https://neal.fun/infinite-craft/'); 50 | console.log('Initialized headless-browser'); 51 | 52 | async function fetchFF(url, options = {}) { 53 | const responseBody = await page.evaluate(async ({url, options}) => { 54 | const fetchResponse = await fetch(url, options); 55 | return await fetchResponse.json(); 56 | }, {url, options}); 57 | 58 | return responseBody; 59 | } 60 | 61 | async function doCraft(ingrA, ingrB, retry = 3) { 62 | console.log(`Start downloading ${ingrA} + ${ingrB}`); 63 | try { 64 | const data = await fetchFF(API_ENDPOINT + new URLSearchParams({ 65 | first: ingrA, 66 | second: ingrB, 67 | }), { 68 | headers: DISGUISE_HEADERS, 69 | }); 70 | // if (data.startsWith(' 100) { 309 | await sleep(200); 310 | } 311 | let res = await requestRecipesDB(i); 312 | if (res == null) { 313 | i -= 1; 314 | await sleep(1000); 315 | continue; 316 | } 317 | 318 | for (let r of res) { 319 | eq.pushBack([r.element1_id, r.element2_id, r.result_element_id]); 320 | } 321 | } 322 | } 323 | 324 | async function buildContributionList() { 325 | const data = JSON.parse(await fs.readFile('./relevant_recipes.json', 'utf-8')); 326 | for (const v of data) { 327 | eq.pushBack(v); 328 | } 329 | } 330 | 331 | function buildBasicExploreList() { 332 | const basics = ['Crafting', 'Time', 'Kernel']; 333 | for (const b of basics) { 334 | const ingrB = load_item_by_handle.get(b); 335 | const rows = possible_explore_items_with.all({ other: ingrB.id }); 336 | for (const a of rows) { 337 | eq.pushBack([a.handle, b]); 338 | } 339 | } 340 | } 341 | 342 | function buildSelfExploreList() { 343 | const rows = possible_self_explore_items.all(); 344 | for (const a of rows) { 345 | eq.pushBack([a.handle, a.handle]); 346 | } 347 | } 348 | 349 | async function buildLoadInFiniteCraftFirstList() { 350 | const raw_data = await fs.readFile('relevant_recipes.json', 'utf8'); 351 | const data = JSON.parse(raw_data); 352 | for (const Rhandle of data.o) { 353 | const r = data.r[Rhandle][0]; 354 | eq.pushBack([r[0], r[1], Rhandle]); 355 | } 356 | console.log(`Loaded ${data.o.length} items from JSON file.`); 357 | // console.log(eq.val.slice(0, 10)); 358 | } 359 | 360 | let iv = null; 361 | async function main(exploreFunc) { 362 | const CO_TASK_MAX = 1, ERR_MAX = 1; 363 | let fail_cnt = 0, task_cnt = 0; 364 | let bucket = new AsyncTokenBucket(CO_TASK_MAX); 365 | await bucket.aquire(); 366 | iv = setInterval(() => { 367 | if (!isExiting && fail_cnt < ERR_MAX) { 368 | if (task_cnt < CO_TASK_MAX) { 369 | bucket.refill(); 370 | } 371 | } else { 372 | clearInterval(iv); 373 | browser.close(); 374 | } 375 | }, 250); 376 | 377 | while (true) { 378 | await bucket.aquire(); 379 | queueMicrotask(async () => { 380 | task_cnt += 1; 381 | while (true) { 382 | if (isExiting) { 383 | break; 384 | } 385 | let x = await exploreFunc(); 386 | if (x < 0) { 387 | fail_cnt += 1; 388 | break; 389 | } 390 | 391 | if (x != 0) { 392 | break; 393 | } 394 | } 395 | task_cnt -= 1; 396 | }); 397 | } 398 | } 399 | 400 | // buildSelfExploreList(); 401 | // buildBasicExploreList(); 402 | // buildRecipesDBExploreList(); 403 | // await buildLoadInFiniteCraftFirstList(); 404 | await buildContributionList(); 405 | main(exploreByQueue); 406 | // console.log(await doCraft('Wig', 'Lizard')); 407 | // console.log(await (await fetch('https://tls.peet.ws/api/all', { 408 | // agent: FF_DISGUISE_AGENT, 409 | // })).json()) 410 | // console.log(await doCraft('Lava', 'Water')); 411 | // for (let i = ; i <= 2025; ++i) { 412 | // if (!await exploreCustom(`Windows ${i}`, 'Last')) { 413 | // break; 414 | // } 415 | // } 416 | // exploreCustom('Arch', 'Linux'); 417 | 418 | process.on('exit', async () => { 419 | db.close(); 420 | await browser.close(); 421 | }); 422 | process.on('SIGHUP', () => clearInterval(iv)); 423 | process.on('SIGINT', () => { 424 | console.log('Exiting...'); 425 | clearInterval(iv); 426 | isExiting = true; 427 | }); 428 | process.on('SIGTERM', () => process.exit(128 + 15)); 429 | -------------------------------------------------------------------------------- /bstruct/type-handler.mjs: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | 3 | /** 4 | * Represents the result of deserialization. 5 | * @class 6 | */ 7 | export class DeserializedResult { 8 | /** 9 | * @constructor 10 | * @param {any} value - The deserialized value. 11 | * @param {number} offset - The offset after deserialization. 12 | */ 13 | constructor(value, offset) { 14 | this.value = value; 15 | this.offset = offset; 16 | } 17 | } 18 | 19 | /** 20 | * Base class for handling basic data types. 21 | * @abstract 22 | * @class 23 | */ 24 | export class BaseTypeHandler { 25 | /** 26 | * Gets the size of the serialized value in bytes. 27 | * @abstract 28 | * @param {any} value - The value to be serialized. 29 | * @returns {number} - The size of the serialized value in bytes. 30 | */ 31 | sizeof(value) { 32 | throw new Error('virtual method called'); 33 | } 34 | 35 | /** 36 | * Serializes the value and writes it to the DataView at the specified offset. 37 | * @abstract 38 | * @param {DataView} view - The DataView to write to. 39 | * @param {number} offset - The offset to start writing at. 40 | * @param {any} value - The value to be serialized. 41 | * @returns {number} - The new offset after serialization. 42 | */ 43 | serialize(view, offset, value) { 44 | throw new Error('virtual method called'); 45 | } 46 | 47 | /** 48 | * Deserializes the value from the DataView at the specified offset. 49 | * @abstract 50 | * @param {DataView} view - The DataView to read from. 51 | * @param {number} offset - The offset to start reading from. 52 | * @returns {DeserializedResult} - The deserialized result. 53 | */ 54 | deserialize(view, offset) { 55 | throw new Error('virtual method called'); 56 | } 57 | } 58 | 59 | /** 60 | * Handles 8-bit signed integers (int8). 61 | * @extends {BaseTypeHandler} 62 | * @class 63 | */ 64 | export class Int8Handler extends BaseTypeHandler { 65 | /** 66 | * Gets the size of the serialized int8 value in bytes (always 1). 67 | * @param {number} _value - The int8 value. 68 | * @returns {number} - The size of the serialized int8 value in bytes. 69 | */ 70 | sizeof(_value) { 71 | return 1; 72 | } 73 | 74 | /** 75 | * Serializes the int8 value and writes it to the DataView at the specified offset. 76 | * @param {DataView} view - The DataView to write to. 77 | * @param {number} offset - The offset to start writing at. 78 | * @param {number} value - The int8 value to be serialized. 79 | * @returns {number} - The new offset after serialization. 80 | */ 81 | serialize(view, offset, value) { 82 | view.setInt8(offset, value); 83 | return offset + 1; 84 | } 85 | 86 | /** 87 | * Deserializes the int8 value from the DataView at the specified offset. 88 | * @param {DataView} view - The DataView to read from. 89 | * @param {number} offset - The offset to start reading from. 90 | * @returns {DeserializedResult} - The deserialized result containing the int8 value and a new offset. 91 | */ 92 | deserialize(view, offset) { 93 | return new DeserializedResult(view.getInt8(offset), offset + 1); 94 | } 95 | } 96 | 97 | /** 98 | * Handles 16-bit signed integers (int16). 99 | * @extends {BaseTypeHandler} 100 | * @class 101 | */ 102 | export class Int16Handler extends BaseTypeHandler { 103 | /** 104 | * Gets the size of the serialized int16 value in bytes (always 2). 105 | * @param {number} _value - The int16 value. 106 | * @returns {number} - The size of the serialized int16 value in bytes. 107 | */ 108 | sizeof(_value) { 109 | return 2; 110 | } 111 | 112 | /** 113 | * Serializes the int16 value and writes it to the DataView at the specified offset. 114 | * @param {DataView} view - The DataView to write to. 115 | * @param {number} offset - The offset to start writing at. 116 | * @param {number} value - The int16 value to be serialized. 117 | * @returns {number} - The new offset after serialization. 118 | */ 119 | serialize(view, offset, value) { 120 | view.setInt16(offset, value, true); 121 | return offset + 2; 122 | } 123 | 124 | /** 125 | * Deserializes the int16 value from the DataView at the specified offset. 126 | * @param {DataView} view - The DataView to read from. 127 | * @param {number} offset - The offset to start reading from. 128 | * @returns {DeserializedResult} - The deserialized result containing the int16 value and a new offset. 129 | */ 130 | deserialize(view, offset) { 131 | return new DeserializedResult(view.getInt16(offset, true), offset + 2); 132 | } 133 | } 134 | 135 | /** 136 | * Handles 32-bit signed integers (int32). 137 | * @extends {BaseTypeHandler} 138 | * @class 139 | */ 140 | export class Int32Handler extends BaseTypeHandler { 141 | /** 142 | * Gets the size of the serialized int32 value in bytes (always 4). 143 | * @param {number} _value - The int32 value. 144 | * @returns {number} - The size of the serialized int32 value in bytes. 145 | */ 146 | sizeof(_value) { 147 | return 4; 148 | } 149 | 150 | /** 151 | * Serializes the int32 value and writes it to the DataView at the specified offset. 152 | * @param {DataView} view - The DataView to write to. 153 | * @param {number} offset - The offset to start writing at. 154 | * @param {number} value - The int32 value to be serialized. 155 | * @returns {number} - The new offset after serialization. 156 | */ 157 | serialize(view, offset, value) { 158 | view.setInt32(offset, value, true); 159 | return offset + 4; 160 | } 161 | 162 | /** 163 | * Deserializes the int32 value from the DataView at the specified offset. 164 | * @param {DataView} view - The DataView to read from. 165 | * @param {number} offset - The offset to start reading from. 166 | * @returns {DeserializedResult} - The deserialized result containing the int32 value and a new offset. 167 | */ 168 | deserialize(view, offset) { 169 | return new DeserializedResult(view.getInt32(offset, true), offset + 4); 170 | } 171 | } 172 | 173 | /** 174 | * Handles 64-bit signed integers (int64). 175 | * @extends {BaseTypeHandler} 176 | * @class 177 | */ 178 | export class Int64Handler extends BaseTypeHandler { 179 | /** 180 | * Gets the size of the serialized int64 value in bytes (always 8). 181 | * @param {BigInt} _value - The int64 value. 182 | * @returns {number} - The size of the serialized int64 value in bytes. 183 | */ 184 | sizeof(_value) { 185 | return 8; 186 | } 187 | 188 | /** 189 | * Serializes the int64 value and writes it to the DataView at the specified offset. 190 | * @param {DataView} view - The DataView to write to. 191 | * @param {number} offset - The offset to start writing at. 192 | * @param {BigInt} value - The int64 value to be serialized. 193 | * @returns {number} - The new offset after serialization. 194 | */ 195 | serialize(view, offset, value) { 196 | view.setBigInt64(offset, value, true); 197 | return offset + 8; 198 | } 199 | 200 | /** 201 | * Deserializes the int64 value from the DataView at the specified offset. 202 | * @param {DataView} view - The DataView to read from. 203 | * @param {number} offset - The offset to start reading from. 204 | * @returns {DeserializedResult} - The deserialized result containing the int64 value and a new offset. 205 | */ 206 | deserialize(view, offset) { 207 | return new DeserializedResult(view.getBigInt64(offset, true), offset + 8); 208 | } 209 | } 210 | 211 | /** 212 | * Handles 8-bit unsigned integers (uint8). 213 | * @extends {BaseTypeHandler} 214 | * @class 215 | */ 216 | export class Uint8Handler extends BaseTypeHandler { 217 | /** 218 | * Gets the size of the serialized uint8 value in bytes (always 1). 219 | * @param {number} _value - The uint8 value. 220 | * @returns {number} - The size of the serialized uint8 value in bytes. 221 | */ 222 | sizeof(_value) { 223 | return 1; 224 | } 225 | 226 | /** 227 | * Serializes the uint8 value and writes it to the DataView at the specified offset. 228 | * @param {DataView} view - The DataView to write to. 229 | * @param {number} offset - The offset to start writing at. 230 | * @param {number} value - The uint8 value to be serialized. 231 | * @returns {number} - The new offset after serialization. 232 | */ 233 | serialize(view, offset, value) { 234 | view.setUint8(offset, value); 235 | return offset + 1; 236 | } 237 | 238 | /** 239 | * Deserializes the uint8 value from the DataView at the specified offset. 240 | * @param {DataView} view - The DataView to read from. 241 | * @param {number} offset - The offset to start reading from. 242 | * @returns {DeserializedResult} - The deserialized result containing the uint8 value and a new offset. 243 | */ 244 | deserialize(view, offset) { 245 | return new DeserializedResult(view.getUint8(offset), offset + 1); 246 | } 247 | } 248 | 249 | /** 250 | * Handles 16-bit unsigned integers (uint16). 251 | * @extends {BaseTypeHandler} 252 | * @class 253 | */ 254 | export class Uint16Handler extends BaseTypeHandler { 255 | /** 256 | * Gets the size of the serialized uint16 value in bytes (always 2). 257 | * @param {number} _value - The uint16 value. 258 | * @returns {number} - The size of the serialized uint16 value in bytes. 259 | */ 260 | sizeof(_value) { 261 | return 2; 262 | } 263 | 264 | /** 265 | * Serializes the uint16 value and writes it to the DataView at the specified offset. 266 | * @param {DataView} view - The DataView to write to. 267 | * @param {number} offset - The offset to start writing at. 268 | * @param {number} value - The uint16 value to be serialized. 269 | * @returns {number} - The new offset after serialization. 270 | */ 271 | serialize(view, offset, value) { 272 | view.setUint16(offset, value, true); 273 | return offset + 2; 274 | } 275 | 276 | /** 277 | * Deserializes the uint16 value from the DataView at the specified offset. 278 | * @param {DataView} view - The DataView to read from. 279 | * @param {number} offset - The offset to start reading from. 280 | * @returns {DeserializedResult} - The deserialized result containing the uint16 value and a new offset. 281 | */ 282 | deserialize(view, offset) { 283 | return new DeserializedResult(view.getUint16(offset, true), offset + 2); 284 | } 285 | } 286 | 287 | /** 288 | * Handles 32-bit unsigned integers (uint32). 289 | * @extends {BaseTypeHandler} 290 | * @class 291 | */ 292 | export class Uint32Handler extends BaseTypeHandler { 293 | /** 294 | * Gets the size of the serialized uint32 value in bytes (always 4). 295 | * @param {number} _value - The uint32 value. 296 | * @returns {number} - The size of the serialized uint32 value in bytes. 297 | */ 298 | sizeof(_value) { 299 | return 4; 300 | } 301 | 302 | /** 303 | * Serializes the uint32 value and writes it to the DataView at the specified offset. 304 | * @param {DataView} view - The DataView to write to. 305 | * @param {number} offset - The offset to start writing at. 306 | * @param {number} value - The uint32 value to be serialized. 307 | * @returns {number} - The new offset after serialization. 308 | */ 309 | serialize(view, offset, value) { 310 | view.setUint32(offset, value, true); 311 | return offset + 4; 312 | } 313 | 314 | /** 315 | * Deserializes the uint32 value from the DataView at the specified offset. 316 | * @param {DataView} view - The DataView to read from. 317 | * @param {number} offset - The offset to start reading from. 318 | * @returns {DeserializedResult} - The deserialized result containing the uint32 value and a new offset. 319 | */ 320 | deserialize(view, offset) { 321 | return new DeserializedResult(view.getUint32(offset, true), offset + 4); 322 | } 323 | } 324 | 325 | /** 326 | * Handles 64-bit unsigned integers (uint64). 327 | * @extends {BaseTypeHandler} 328 | * @class 329 | */ 330 | export class Uint64Handler extends BaseTypeHandler { 331 | /** 332 | * Gets the size of the serialized uint64 value in bytes (always 8). 333 | * @param {BigInt} _value - The uint64 value. 334 | * @returns {number} - The size of the serialized uint64 value in bytes. 335 | */ 336 | sizeof(_value) { 337 | return 8; 338 | } 339 | 340 | /** 341 | * Serializes the uint64 value and writes it to the DataView at the specified offset. 342 | * @param {DataView} view - The DataView to write to. 343 | * @param {number} offset - The offset to start writing at. 344 | * @param {BigInt} value - The uint64 value to be serialized. 345 | * @returns {number} - The new offset after serialization. 346 | */ 347 | serialize(view, offset, value) { 348 | view.setBigUint64(offset, value, true); 349 | return offset + 8; 350 | } 351 | 352 | /** 353 | * Deserializes the uint64 value from the DataView at the specified offset. 354 | * @param {DataView} view - The DataView to read from. 355 | * @param {number} offset - The offset to start reading from. 356 | * @returns {DeserializedResult} - The deserialized result containing the uint64 value and a new offset. 357 | */ 358 | deserialize(view, offset) { 359 | return new DeserializedResult(view.getBigUint64(offset, true), offset + 8); 360 | } 361 | } 362 | 363 | /** 364 | * Handles 32-bit floating point numbers (float32). 365 | * @extends {BaseTypeHandler} 366 | * @class 367 | */ 368 | export class Float32Handler extends BaseTypeHandler { 369 | /** 370 | * Gets the size of the serialized float32 value in bytes (always 4). 371 | * @param {number} _value - The float32 value. 372 | * @returns {number} - The size of the serialized float32 value in bytes. 373 | */ 374 | sizeof(_value) { 375 | return 4; 376 | } 377 | 378 | /** 379 | * Serializes the float32 value and writes it to the DataView at the specified offset. 380 | * @param {DataView} view - The DataView to write to. 381 | * @param {number} offset - The offset to start writing at. 382 | * @param {number} value - The float32 value to be serialized. 383 | * @returns {number} - The new offset after serialization. 384 | */ 385 | serialize(view, offset, value) { 386 | view.setFloat32(offset, value); 387 | return offset + 4; 388 | } 389 | 390 | /** 391 | * Deserializes the float32 value from the DataView at the specified offset. 392 | * @param {DataView} view - The DataView to read from. 393 | * @param {number} offset - The offset to start reading from. 394 | * @returns {DeserializedResult} - The deserialized result containing the float32 value and a new offset. 395 | */ 396 | deserialize(view, offset) { 397 | return new DeserializedResult(view.getFloat32(offset), offset + 4); 398 | } 399 | } 400 | 401 | /** 402 | * Handles 64-bit floating point numbers (float64). 403 | * @extends {BaseTypeHandler} 404 | * @class 405 | */ 406 | export class Float64Handler extends BaseTypeHandler { 407 | /** 408 | * Gets the size of the serialized float64 value in bytes (always 8). 409 | * @param {number} _value - The float64 value. 410 | * @returns {number} - The size of the serialized float64 value in bytes. 411 | */ 412 | sizeof(_value) { 413 | return 8; 414 | } 415 | 416 | /** 417 | * Serializes the float64 value and writes it to the DataView at the specified offset. 418 | * @param {DataView} view - The DataView to write to. 419 | * @param {number} offset - The offset to start writing at. 420 | * @param {number} value - The float64 value to be serialized. 421 | * @returns {number} - The new offset after serialization. 422 | */ 423 | serialize(view, offset, value) { 424 | view.setFloat64(offset, value); 425 | return offset + 8; 426 | } 427 | 428 | /** 429 | * Deserializes the float64 value from the DataView at the specified offset. 430 | * @param {DataView} view - The DataView to read from. 431 | * @param {number} offset - The offset to start reading from. 432 | * @returns {DeserializedResult} - The deserialized result containing the float64 value and a new offset. 433 | */ 434 | deserialize(view, offset) { 435 | return new DeserializedResult(view.getFloat64(offset), offset + 8); 436 | } 437 | } 438 | 439 | /** 440 | * Handles boolean values (bool). 441 | * @extends {BaseTypeHandler} 442 | * @class 443 | */ 444 | export class BoolHandler extends BaseTypeHandler { 445 | /** 446 | * Gets the size of the serialized bool value in bytes (always 1). 447 | * @param {boolean} _value - The bool value. 448 | * @returns {number} - The size of the serialized bool value in bytes. 449 | */ 450 | sizeof(_value) { 451 | return 1; 452 | } 453 | 454 | /** 455 | * Serializes the bool value and writes it to the DataView at the specified offset. 456 | * @param {DataView} view - The DataView to write to. 457 | * @param {number} offset - The offset to start writing at. 458 | * @param {boolean} value - The bool value to be serialized. 459 | * @returns {number} - The new offset after serialization. 460 | */ 461 | serialize(view, offset, value) { 462 | view.setUint8(offset, value ? 1 : 0); 463 | return offset + 1; 464 | } 465 | 466 | /** 467 | * Deserializes the bool value from the DataView at the specified offset. 468 | * @param {DataView} view - The DataView to read from. 469 | * @param {number} offset - The offset to start reading from. 470 | * @returns {DeserializedResult} - The deserialized result containing the bool value and a new offset. 471 | */ 472 | deserialize(view, offset) { 473 | return new DeserializedResult(view.getUint8(offset) !== 0, offset + 1); 474 | } 475 | } 476 | 477 | /** 478 | * Handles boolean values (bool). 479 | * @extends {BaseTypeHandler} 480 | * @class 481 | */ 482 | export class DateHandler extends BaseTypeHandler { 483 | /** 484 | * Gets the size of the serialized Date value in bytes (always 8). 485 | * @param {Date} _value - The bool value. 486 | * @returns {number} - The size of the serialized bool value in bytes. 487 | */ 488 | sizeof(_value) { 489 | return 8; 490 | } 491 | 492 | /** 493 | * Serializes the Date value and writes it to the DataView at the specified offset. 494 | * @param {DataView} view - The DataView to write to. 495 | * @param {number} offset - The offset to start writing at. 496 | * @param {Date} value - The Date value to be serialized. 497 | * @returns {number} - The new offset after serialization. 498 | */ 499 | serialize(view, offset, value) { 500 | view.setBigUint64(offset, BigInt(Math.floor(value.getTime() / 1000)), true); 501 | return offset + 8; 502 | } 503 | 504 | /** 505 | * Deserializes the Date value from the DataView at the specified offset. 506 | * @param {DataView} view - The DataView to read from. 507 | * @param {number} offset - The offset to start reading from. 508 | * @returns {DeserializedResult} - The deserialized result containing the Date value and a new offset. 509 | */ 510 | deserialize(view, offset) { 511 | const timestamp = parseInt(view.getBigUint64(offset, true)); 512 | return new DeserializedResult(new Date(timestamp * 1000), offset + 8); 513 | } 514 | } 515 | 516 | /** 517 | * Handles void values (void). 518 | * @extends {BaseTypeHandler} 519 | * @class 520 | */ 521 | export class VoidHandler extends BaseTypeHandler { 522 | /** 523 | * Gets the size of the serialized void value in bytes (always 0). 524 | * @param {*} _value - The void value. 525 | * @returns {number} - The size of the serialized void value in bytes (always 0). 526 | */ 527 | sizeof(_value) { 528 | return 0; 529 | } 530 | 531 | /** 532 | * Serializes the void value (does nothing). 533 | * @param {DataView} _view - The DataView to write to (not used). 534 | * @param {number} offset - The offset to start writing at (not used). 535 | * @param {*} _value - The void value (not used). 536 | * @returns {number} - The offset unchanged. 537 | */ 538 | serialize(_view, offset, _value) { 539 | return offset; 540 | } 541 | 542 | /** 543 | * Deserializes the void value (does nothing). 544 | * @param {DataView} _view - The DataView to read from (not used). 545 | * @param {number} offset - The offset to start reading from (not used). 546 | * @returns {DeserializedResult} - The offset unchanged and undefined as the value. 547 | */ 548 | deserialize(_view, offset) { 549 | return new DeserializedResult(undefined, offset); 550 | } 551 | } 552 | 553 | function getHandlerObject(type) { 554 | if (type instanceof BaseTypeHandler) { 555 | return type; 556 | } 557 | return new CompoundTypeHandler(type); 558 | } 559 | 560 | /** 561 | * Handles array of a fixed length with elements of the same type. 562 | * @extends {BaseTypeHandler} 563 | * @class 564 | */ 565 | export class FixedArrayHandler extends BaseTypeHandler { 566 | /** 567 | * Constructor for FixedArrayHandler. 568 | * @param {number} n - The fixed length of the array. 569 | * @param {BaseTypeHandler} element_handler - The handler for individual elements of the array. 570 | */ 571 | constructor(n, element_handler) { 572 | super(); 573 | this.n = n; 574 | this.element_handler = getHandlerObject(element_handler); 575 | } 576 | 577 | /** 578 | * Gets the size of the serialized fixed-length array in bytes. 579 | * @param {Array} value - The array to calculate the size for. 580 | * @returns {number} - The size of the serialized fixed-length array in bytes. 581 | */ 582 | sizeof(value) { 583 | let res = 0; 584 | for (let i = 0; i < this.n; i += 1) { 585 | res += this.element_handler.sizeof(value[i]); 586 | } 587 | return res; 588 | } 589 | 590 | /** 591 | * Serializes the fixed-length array and writes it to the DataView at the specified offset. 592 | * @param {DataView} view - The DataView to write to. 593 | * @param {number} offset - The offset to start writing at. 594 | * @param {Array} value - The fixed-length array to be serialized. 595 | * @returns {number} - The new offset after serialization. 596 | */ 597 | serialize(view, offset, value) { 598 | for (let i = 0; i < this.n; i += 1) { 599 | offset = this.element_handler.serialize(view, offset, value[i]); 600 | } 601 | return offset; 602 | } 603 | 604 | /** 605 | * Deserializes the fixed-length array from the DataView at the specified offset. 606 | * @param {DataView} view - The DataView to read from. 607 | * @param {number} offset - The offset to start reading from. 608 | * @returns {DeserializedResult} - The deserialized result containing the fixed-length array and a new offset. 609 | */ 610 | deserialize(view, offset) { 611 | let res = new Array(this.n); 612 | for (let i = 0; i < this.n; i += 1) { 613 | const tmp = this.element_handler.deserialize(view, offset); 614 | res[i] = tmp.value; 615 | offset = tmp.offset; 616 | } 617 | return new DeserializedResult(res, offset); 618 | } 619 | } 620 | 621 | /** 622 | * Handles raw binary buffer of a fixed length. 623 | * @extends {BaseTypeHandler} 624 | * @class 625 | */ 626 | export class RawBufferHandler extends BaseTypeHandler { 627 | /** 628 | * Constructor for RawBufferHandler. 629 | * @param {number} n - The fixed length of the buffer. 630 | */ 631 | constructor(n) { 632 | super(); 633 | this.n = n; 634 | } 635 | 636 | /** 637 | * Gets the size of the serialized fixed-length buffer in bytes. 638 | * @param {Buffer} value - The array to calculate the size for. 639 | * @returns {number} - The size of the serialized fixed-length array in bytes. 640 | */ 641 | sizeof(value) { 642 | return value.byteLength; 643 | } 644 | 645 | /** 646 | * Serializes the fixed-length buffer and writes it to the DataView at the specified offset. 647 | * @param {DataView} view - The DataView to write to. 648 | * @param {number} offset - The offset to start writing at. 649 | * @param {Buffer} value - The fixed-length buffer to be serialized. 650 | * @returns {number} - The new offset after serialization. 651 | */ 652 | serialize(view, offset, value) { 653 | for (let i = 0; i < this.n; i += 1) { 654 | view.setUint8(offset + i, value.readUInt8(offset + i)); 655 | } 656 | return offset + this.n; 657 | } 658 | 659 | /** 660 | * Deserializes the fixed-length array from the DataView at the specified offset. 661 | * @param {DataView} view - The DataView to read from. 662 | * @param {number} offset - The offset to start reading from. 663 | * @returns {DeserializedResult} - The deserialized result containing the fixed-length array and a new offset. 664 | */ 665 | deserialize(view, offset) { 666 | const res = Buffer.from(view.buffer, offset, this.n); 667 | return new DeserializedResult(res, offset + this.n); 668 | } 669 | } 670 | 671 | /** 672 | * Handles dynamic arrays with elements of the same type. 673 | * @extends {BaseTypeHandler} 674 | * @class 675 | */ 676 | export class DynamicArrayHandler extends BaseTypeHandler { 677 | /** 678 | * Constructor for DynamicArrayHandler. 679 | * @param {BaseTypeHandler} element_handler - The handler for individual elements of the array. 680 | */ 681 | constructor(element_handler) { 682 | super(); 683 | this.element_handler = getHandlerObject(element_handler); 684 | } 685 | 686 | /** 687 | * Gets the size of the serialized dynamic array in bytes. 688 | * @param {Array} value - The array to calculate the size for. 689 | * @returns {number} - The size of the serialized dynamic array in bytes. 690 | */ 691 | sizeof(value) { 692 | let size = 4; // For storing the length of the array 693 | for (const element of value) { 694 | size += this.element_handler.sizeof(element); 695 | } 696 | return size; 697 | } 698 | 699 | /** 700 | * Serializes the dynamic array and writes it to the DataView at the specified offset. 701 | * @param {DataView} view - The DataView to write to. 702 | * @param {number} offset - The offset to start writing at. 703 | * @param {Array} value - The dynamic array to be serialized. 704 | * @returns {number} - The new offset after serialization. 705 | */ 706 | serialize(view, offset, value) { 707 | view.setUint32(offset, value.length, true); 708 | offset += 4; 709 | for (const element of value) { 710 | offset = this.element_handler.serialize(view, offset, element); 711 | } 712 | return offset; 713 | } 714 | 715 | /** 716 | * Deserializes the dynamic array from the DataView at the specified offset. 717 | * @param {DataView} view - The DataView to read from. 718 | * @param {number} offset - The offset to start reading from. 719 | * @returns {DeserializedResult} - The deserialized result containing the dynamic array and a new offset. 720 | */ 721 | deserialize(view, offset) { 722 | const length = view.getUint32(offset, true); 723 | offset += 4; 724 | const res = new Array(length); 725 | for (let i = 0; i < length; i++) { 726 | const tmp = this.element_handler.deserialize(view, offset); 727 | res[i] = tmp.value; 728 | offset = tmp.offset; 729 | } 730 | return new DeserializedResult(res, offset); 731 | } 732 | } 733 | 734 | /** 735 | * Handles map with keys and values are of the same type. 736 | * @extends {BaseTypeHandler} 737 | * @class 738 | */ 739 | export class MapHandler extends BaseTypeHandler { 740 | /** 741 | * Constructor for MapHandler. 742 | * @param {BaseTypeHandler} key_handler - The handler for keys of the map. 743 | * @param {BaseTypeHandler} value_handler - The handler for values of the map. 744 | */ 745 | constructor(key_handler, value_handler) { 746 | super(); 747 | this.key_handler = getHandlerObject(key_handler); 748 | this.value_handler = getHandlerObject(value_handler); 749 | } 750 | 751 | /** 752 | * Gets the size of the serialized map in bytes. 753 | * @param {Map} value - The map to calculate the size for. 754 | * @returns {number} - The size of the serialized map in bytes. 755 | */ 756 | sizeof(value) { 757 | let res = 4; 758 | for (const [k, v] of value) { 759 | res += this.key_handler.sizeof(k); 760 | res += this.value_handler.sizeof(v); 761 | } 762 | return res; 763 | } 764 | 765 | /** 766 | * Serializes the map and writes it to the DataView at the specified offset. 767 | * @param {DataView} view - The DataView to write to. 768 | * @param {number} offset - The offset to start writing at. 769 | * @param {Map} value - The map to be serialized. 770 | * @returns {number} - The new offset after serialization. 771 | */ 772 | serialize(view, offset, value) { 773 | view.setUint32(offset, value.size, true); 774 | offset += 4; 775 | for (const [k, v] of value) { 776 | offset = this.key_handler.serialize(view, offset, k); 777 | offset = this.value_handler.serialize(view, offset, v); 778 | } 779 | return offset; 780 | } 781 | 782 | /** 783 | * Deserializes the map from the DataView at the specified offset. 784 | * @param {DataView} view - The DataView to read from. 785 | * @param {number} offset - The offset to start reading from. 786 | * @returns {DeserializedResult} - The deserialized result containing the map and a new offset. 787 | */ 788 | deserialize(view, offset) { 789 | const size = view.getUint32(offset, true); 790 | offset += 4; 791 | const res = new Map(); 792 | for (let i = 0; i < size; i += 1) { 793 | const resk = this.key_handler.deserialize(view, offset); 794 | const resv = this.value_handler.deserialize(view, resk.offset); 795 | offset = resv.offset; 796 | res.set(resk.value, resv.value); 797 | } 798 | return new DeserializedResult(res, offset); 799 | } 800 | } 801 | 802 | /** 803 | * Handles storage and serialization of strings. 804 | * @extends {BaseTypeHandler} 805 | * @class 806 | */ 807 | export class StringHandler extends BaseTypeHandler { 808 | /** 809 | * Gets the size of the serialized string in bytes. 810 | * @param {string} value - The string to calculate the size for. 811 | * @returns {number} - The size of the serialized string in bytes. 812 | */ 813 | sizeof(value) { 814 | // Convert the string to UTF-8 encoding and calculate its byte length 815 | const encoder = new TextEncoder(); 816 | const encodedString = encoder.encode(value); 817 | 818 | // Calculate the size of the string (length of UTF-8 encoding) plus 4 bytes for storing the length 819 | return encodedString.byteLength + 4; 820 | } 821 | 822 | /** 823 | * Serializes the string and writes it to the DataView at the specified offset. 824 | * @param {DataView} view - The DataView to write to. 825 | * @param {number} offset - The offset to start writing at. 826 | * @param {string} value - The string to be serialized. 827 | * @returns {number} - The new offset after serialization. 828 | */ 829 | serialize(view, offset, value) { 830 | // Convert the string to UTF-8 encoding 831 | const encoder = new TextEncoder(); 832 | const encoded_string = encoder.encode(value); 833 | 834 | // Write the length of the string as a uint32 at the specified offset 835 | view.setUint32(offset, encoded_string.length, true); 836 | offset += 4; 837 | 838 | // Write the UTF-8 encoded string to the DataView starting at offset + 4 (after the length) 839 | for (let i = 0; i < encoded_string.length; i++) { 840 | view.setUint8(offset + i, encoded_string[i]); 841 | } 842 | 843 | // Return the new offset after serialization 844 | return offset + encoded_string.length; 845 | } 846 | 847 | /** 848 | * Deserializes the string from the DataView at the specified offset. 849 | * @param {DataView} view - The DataView to read from. 850 | * @param {number} offset - The offset to start reading from. 851 | * @returns {DeserializedResult} - The deserialized result containing the string and a new offset. 852 | */ 853 | deserialize(view, offset) { 854 | // Read the length of the string as a uint32 at the specified offset 855 | const length = view.getUint32(offset, true); 856 | offset += 4; 857 | 858 | // Read the UTF-8 encoded string from the DataView starting at offset + 4 (after the length) 859 | const encoded_string = new Uint8Array(view.buffer, offset, length); 860 | 861 | // Convert the UTF-8 encoded string to a JavaScript string 862 | const decoder = new TextDecoder(); 863 | const decodedString = decoder.decode(encoded_string); 864 | 865 | // Return the deserialized string and the new offset 866 | return new DeserializedResult(decodedString, offset + length); 867 | } 868 | } 869 | 870 | /** 871 | * Handles the serialization and deserialization of a compound type composed of various fields. 872 | * @extends {BaseTypeHandler} 873 | * @class 874 | */ 875 | export class CompoundTypeHandler extends BaseTypeHandler { 876 | /** 877 | * Constructs a CompoundTypeHandler object for the specified type. 878 | * @param {Function} type - The class representing the compound type. 879 | * @example 880 | * // Define a class Point 881 | * class Point { 882 | * constructor(x, y) { 883 | * this.x = x; 884 | * this.y = y; 885 | * } 886 | * } 887 | * 888 | * // Define the type definition for Point 889 | * Point.typedef = [ 890 | * {field: 'x', type: BASIC_TYPES.f32}, 891 | * {field: 'y', type: BASIC_TYPES.f32} 892 | * ]; 893 | * 894 | * // Create a CompoundTypeHandler for Point 895 | * const pointHandler = new CompoundTypeHandler(Point); 896 | */ 897 | constructor(type) { 898 | super(); 899 | /** 900 | * The class representing the compound type. 901 | * @type {Function} 902 | * @private 903 | */ 904 | this.type = type; 905 | /** 906 | * The type definition specifying the fields and their handlers. 907 | * @type {Array<{field: string, type: BaseTypeHandler}>} 908 | * @private 909 | */ 910 | this.typedef = type.typedef; 911 | } 912 | 913 | /** 914 | * Gets the size of the serialized compound type in bytes. 915 | * @param {Object} value - The instance of the compound type to calculate the size for. 916 | * @returns {number} - The size of the serialized compound type in bytes. 917 | */ 918 | sizeof(value) { 919 | let res = 0; 920 | for (let i = 0; i < this.typedef.length; i += 1) { 921 | const field_name = this.typedef[i].field; 922 | const field_handler = getHandlerObject(this.typedef[i].type); 923 | res += field_handler.sizeof(value[field_name]); 924 | } 925 | return res; 926 | } 927 | 928 | /** 929 | * Serializes the compound type and writes it to the DataView at the specified offset. 930 | * @param {DataView} view - The DataView to write to. 931 | * @param {number} offset - The offset to start writing at. 932 | * @param {Object} value - The instance of the compound type to be serialized. 933 | * @returns {number} - The new offset after serialization. 934 | */ 935 | serialize(view, offset, value) { 936 | for (let i = 0; i < this.typedef.length; i += 1) { 937 | const field_name = this.typedef[i].field; 938 | const field_handler = getHandlerObject(this.typedef[i].type); 939 | offset = field_handler.serialize(view, offset, value[field_name]); 940 | } 941 | return offset; 942 | } 943 | 944 | /** 945 | * Deserializes the compound type from the DataView at the specified offset. 946 | * @param {DataView} view - The DataView to read from. 947 | * @param {number} offset - The offset to start reading from. 948 | * @returns {DeserializedResult} - The deserialized result containing the compound type and a new offset. 949 | */ 950 | deserialize(view, offset) { 951 | let res = new this.type(); 952 | for (let i = 0; i < this.typedef.length; i += 1) { 953 | const field_name = this.typedef[i].field; 954 | const field_handler = getHandlerObject(this.typedef[i].type); 955 | const tmp = field_handler.deserialize(view, offset); 956 | res[field_name] = tmp.value; 957 | offset = tmp.offset; 958 | } 959 | return new DeserializedResult(res, offset); 960 | } 961 | } 962 | 963 | export const BASIC_TYPES = { 964 | i8: new Int8Handler(), 965 | i16: new Int16Handler(), 966 | i32: new Int32Handler(), 967 | i64: new Int64Handler(), 968 | 969 | u8: new Uint8Handler(), 970 | u16: new Uint16Handler(), 971 | u32: new Uint32Handler(), 972 | u64: new Uint64Handler(), 973 | 974 | f32: new Float64Handler(), 975 | f64: new Float64Handler(), 976 | 977 | bool: new BoolHandler(), 978 | void: new VoidHandler(), 979 | str: new StringHandler(), 980 | DateTime: new DateHandler(), 981 | 982 | array: (type) => new DynamicArrayHandler(type), 983 | FixedArray: (n, type) => new FixedArrayHandler(n, type), 984 | raw: (n) => new RawBufferHandler(n), 985 | map: (k, v) => new MapHandler(k, v), 986 | StringMap: new MapHandler(new StringHandler(), new StringHandler()), 987 | }; 988 | --------------------------------------------------------------------------------