Compare commits

...

60 Commits

Author SHA1 Message Date
cbebfa86e7
chore: Add language dropdown for localization
- Added a new language dropdown component for localization
- Improved spacing and styling across multiple components
- Removed unused code and commented out debug lines
- Updated dependencies for flag icons and solid-js library.
2023-05-04 16:16:15 +01:00
2b9a2720bc
refactor(all-in-one): Organize DomainTypes into Flag objects and change type in handleConvertion
- Refactored DomainTypes to a list of Flag objects for better organization
- Changed the domain parameter type in handleConvertion from DomainTypes to string, to remove unnecessary coupling from code.
2023-05-04 16:16:00 +01:00
1b14be445e
refactor(markdown): update README 2023-05-04 16:14:30 +01:00
341f0fae65
feat(hooks): add useLocalStorage 2023-05-04 16:14:11 +01:00
40b9f29e03
refactor(config/Endpoints): clean up 2023-05-04 16:13:54 +01:00
28ac1597c9
feat(components): implement LangDropdown 2023-05-04 16:13:24 +01:00
a2180bed42
feat(components): implement Flag 2023-05-04 16:12:13 +01:00
f6bd3e8ab7
refactor(components/layout): replace useOutSide hook with new component 2023-05-04 14:02:15 +01:00
636ebe8099
build(deps): add flag-icons + update latest 2023-05-04 14:00:57 +01:00
8d9bf58019
refactor(IDE): add more extensions, fix tailwind intellisense 2023-05-02 17:02:58 +01:00
87e13f71f5
fix(styles): animation issues 2023-05-02 17:02:31 +01:00
5b86ebbf7e
fix(styles): resolve font not found 2023-05-02 16:21:09 +01:00
656ce45d44
build(deps): update latest 2023-05-02 15:51:31 +01:00
90720c34f9
feat(all-in-one): update prettier config, enable format on save
Update the VSCode settings.json to include the Prettier VSCode formatter, and enable formatting on save. A new LICENSE file is added to comply with the MIT license due to the open-source nature of the project. The README.md file has now core features and to-dos that should be taken care of as the project continues to evolve. The index.html file is modified to match changes in src/index.tsx. The postcss configuration files are created, and some files are updated. Finally, new files like build.rs, draworder.json, and commands.rs were created
2023-05-02 15:51:22 +01:00
d26b429ee8
refactor(components)
BREAKING CHANGES
- Refactored `App.tsx` to import `Component` from `solid-js`, and use a new component `TitleBar`.
- Added new component `AnimateView` under `src/components/design`, and renamed old `AnimateView` to `Loader`.
- Added new component `Button` under `src/components/design`.
- Added new component `Image` under `src/components/design`.
- Moved old `Image`, `Loader`, and `Button` components under `src/components/design`.
- Refactored `Downloader` component to use `Motion` from `@motionone/solid`, moved it under `Downloader` folder, and used it to create a slick hover effect.
- Removed `Downloaders/Button`.

Notes:
- Used `createSignal` instead of `useState` for SolidJS in `Downloaders.tsx`.
- Used `type` keyword to explicitly define the type of the component props or objects where it makes the code clearer.
2023-05-02 15:49:05 +01:00
6ba06e09d2
feat(components|public): Add Design Loader and change Downloaders popup style
This commit adds a Design Loader to Downloaders component and modifies the popup  style with new classes. The loader is shown when the user clicks the Download GameData button. The popup is now centered and uses flex to position its content. Additionally, it now displays a message that depends on the state of the download process, and the close button's style is also modified.
2023-04-28 14:42:51 +01:00
ea7c2cbf78
refactor(tauri): Update tauri config permissions
The "fs-remove-dir" feature has been added to Tauri dependencies, enabling the directory deletion operation. Similar changes were made to the tauri.conf.json file.
2023-04-28 14:42:10 +01:00
fbf1a5dc7b
refactor(controllers): classes and parse libraries, sets and palettes
- Refactored several classes in controllers directory and modified parsing of libraries, sets and palettes.
2023-04-28 14:41:16 +01:00
a25d278115
refactor(tools): parsing - writing games data
This commit refactors the code to parse and write data from WoW game files. It creates a new function `parseData` which correctly parses the data and writes the output files to a new folder called `output`. The code now handles both `JSON` and `XML` data from game files. It also makes use of optional chaining to prevent null pointers while parsing the data. Finally, it removes an unused `callback` message without any effect.
2023-04-28 14:40:45 +01:00
868e675f8e
refactor(config): organization/naming conventions
- Modified Convertion.ts to simplify directory configuration into outputDir, gamedataDir, and genericDir for versioned folders, and removed gamedataConfigDir.
- Updated Endpoints.ts to include furniture data source from Habbo using JSON format.
2023-04-28 14:35:52 +01:00
1206af1e1d
feat(components/design): implement Loader 2023-04-28 14:34:46 +01:00
2b2a5483e5
refactor(types): enhance + switch from static types to KeyValuePairs type definitions 2023-04-28 12:57:23 +01:00
7badc5a83b
refactor(cargo): update perms config 2023-04-23 23:29:26 +01:00
069a37b550
fix(tools): fs-path for parseData 2023-04-23 23:29:02 +01:00
cb66593b2c
refactor(tools): rename fetchGamedataConfig 2023-04-23 23:28:28 +01:00
4f6b2045f2
refactor(controllers): add data parser 2023-04-23 23:27:49 +01:00
51ba8d4fcb
feat(config): implement Convertion 2023-04-23 23:26:49 +01:00
b26459feeb
refactor: clean up 2023-04-23 20:18:16 +01:00
a946baf1d7
fix(indexes): missing index for imports 2023-04-23 20:17:57 +01:00
2909cbe3c4
fix(types/Converters): remove furniType property 2023-04-23 20:17:33 +01:00
1919787c11
feat(hooks): implement useOutsideClickEventHandler 2023-04-23 20:16:56 +01:00
052ad1c6ab
feat(controllers): integrate EffectMap/FigureData/FigureMap/FurniData 2023-04-23 20:16:45 +01:00
63d9921dd5
refactor(types/Endpoint): add parser xml return type 2023-04-23 20:15:23 +01:00
22ecfdd9a5
feat(types): add global types 2023-04-23 20:14:32 +01:00
6e253823ba
feat(components/system): implement Window 2023-04-23 20:14:12 +01:00
4b9290636f
feat(components/system): implement TitleBar 2023-04-23 20:14:05 +01:00
2771e7b4cc
feat(components/system): implement Button 2023-04-23 20:13:57 +01:00
1272b00014
feat(components/system): implement Image 2023-04-23 20:13:52 +01:00
3ae150f448
feat(components/system): implement AnimatedView 2023-04-23 20:13:44 +01:00
508e999a0c
feat(components/system): implement Popup 2023-04-23 20:13:36 +01:00
7d5b2cbc89
refactor(Downloader(s)): seperate components 2023-04-23 20:13:12 +01:00
bdcf825de7
refactor(all-in-one): clean up 2023-04-23 20:12:45 +01:00
01adee763f
refactor(tauri-config): update window config with permissions 2023-04-23 20:12:05 +01:00
19637f44e7
fix(package.json): typo 2023-04-23 20:09:30 +01:00
bb0c6f32f8
build(deps/cargo): update latest 2023-04-23 20:09:18 +01:00
195bb2c360
build(deps): update latest 2023-04-23 20:06:22 +01:00
ef9c094e5d
fix(tsconfig): d.ts type checking was disabked 2023-04-23 20:06:09 +01:00
90ee57c67d
build(deps): update latest 2023-04-20 16:26:57 +00:00
deb1ca5ad9
fix(components): render UI 2023-04-20 15:56:49 +00:00
9652ec9122
refactor(components): add window dragging + remove resize 2023-04-20 15:56:29 +00:00
32dfc41589
fix(src): temp unused vars 2023-04-19 17:16:05 +00:00
4b4f58a426
refactor(components): enhance Downloader UI 2023-04-19 17:15:11 +00:00
b8dcbc4f5d
refactor(config-types): clean up 2023-04-19 17:14:55 +00:00
c058b1d1e7
feat(mapping): add convertTXT 2023-04-19 17:14:34 +00:00
36ea248c9f
refactor(utils): clean up fetchGameData 2023-04-19 17:14:11 +00:00
9a1feb829c
fix(vscode): remove ignored IDE settings 2023-04-19 17:12:50 +00:00
243b9f3d36
refactor(tauri-config): Allowlist for the path APIs. 2023-04-19 17:12:21 +00:00
a4f1ed4694
build(deps): update latest 2023-04-19 17:11:43 +00:00
0315f32a25
test(config): implement registered keys 2023-04-07 20:44:17 +00:00
8dadf20a64
test(node): build generated types 2023-04-07 18:56:38 +00:00
92 changed files with 3322 additions and 1564 deletions

2
.gitignore vendored
View File

@ -13,8 +13,6 @@ dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo

View File

@ -1,3 +1,9 @@
{
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"bradlc.vscode-tailwindcss",
"DavidAnson.vscode-markdownlint",
"solidjs-community.solid-snippets"
]
}

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"tailwindCSS.classAttributes": ["class", "className"]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2023 Rypî Dev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

94
README.md Normal file
View File

@ -0,0 +1,94 @@
# 🧿 Getting started
## 🎯 Todos
- [ ] Two options for downloading Habbo resources:
- [ ] Default data extraction. (Without conversion)
- [ ] Using [Scuti](https://scuti.netlify.app/) dataset for its [renderer.](https://github.com/kozennnn/scuti-renderer)
- [x] Languages feature. (Default: EN)
- [ ] Download Habbo assets. (>800 MB of disk memory, make sure the IPC handles it correctly)
- [x] Fix rendering animation issues.
- [ ] Add "abort" button with data suppression feature.
- [x] Convert front-end with SolidJS
- [x] Fix issue of prettier-plugin-tailwindcss
- [ ] Export helpers/utils methods into Rust:
- [ ] Implementing typesafety with types' collections using Tauri Specta
- [ ] Handling extracted data in:
- [ ] JSON (Default + Adjustements for Scuti)
- [ ] XML (Using quickxml_to_serde)
- [ ] TXT (no idea...)
- [ ] Parsing data using `std::{fs::File, io::Write}`
- [ ] Learning Rust:
- [ ] Getting to know about vectors in depth.
- [ ] Avoid conflicts in open-source.
- [ ] Undertsand: Memory cache and disk cache.
## Overview
- [How it works](#🏗️-how-it-works)
- [Techs stack](#📦-techs-stack)
- [Interesting topics](#📤-interesting-topics)
- [Credits](#🤝-credits)
## 📦 Techs stack
![Rust lang](https://img.shields.io/badge/Rust-black?style=for-the-badge&logo=rust&logoColor=#E57324)
![Typescript lang](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![Tauri](https://img.shields.io/badge/tauri-%2324C8DB.svg?style=for-the-badge&logo=tauri&logoColor=%23FFFFFF)
![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E)
![SolidJS](https://img.shields.io/badge/SolidJS-2c4f7c?style=for-the-badge&logo=solid&logoColor=c8c9cb)
![Tailwind framework](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white)
![Eslint linter](https://img.shields.io/badge/eslint-3A33D1?style=for-the-badge&logo=eslint&logoColor=white)
![Prettier formatter](https://img.shields.io/badge/prettier-1A2C34?style=for-the-badge&logo=prettier&logoColor=F7BA3E)
## 🏗️ How it works
### Gamedata/Generic
It comes first with fetching data using the `@tauri-apps/api/http` module.
Converting uncoming data into minified JSON file formats.
(No need for avro, parquet, protobuf nor CSV for efficient data compression
and encoding schemas for fast data storing/retrieval)
## 📤 Interesting topics
### 🦀 Rust-related
- [Why serde_json crate is the best choice?](https://blog.logrocket.com/json-and-rust-why-serde_json-is-the-top-choice/)
### Misc
```shell
npx cloc . \
--exclude-dir=target,_site,node_modules \
--not-match-f=pnpm-lock.yaml
```
```shell
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
TypeScript 43 158 19 855
JSON 6 0 0 138
Rust 5 39 81 83
Markdown 1 19 0 45
CSS 2 4 0 30
TOML 1 5 2 27
SVG 1 0 0 24
JavaScript 3 0 1 23
HTML 1 2 0 11
-------------------------------------------------------------------------------
SUM: 63 227 103 1236
-------------------------------------------------------------------------------
```
## 🤝 Credits
This wouldn't be possible without [KOZEN](https://github.com/kozennnn)'s help and bare bones the

View File

@ -8,6 +8,6 @@
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="/src/index.tsx" type="module"></script>
</body>
</html>

View File

@ -2,49 +2,48 @@
"name": "@rypidev/rypi-scrapper",
"private": true,
"license": "MIT",
"type": "module",
"version": "0.0.0",
"description": "",
"description": "Scrapping Habbo gamedata and assets",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
"dev": "vite dev",
"build": "vite build",
"dev:tauri": "vite --config vite.config.tauri.ts",
"build:tauri": "vite build --config vite.config.tauri.ts",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/press-start-2p": "^4.5.11",
"@motionone/solid": "10.16.0",
"@tauri-apps/api": "^1.2.0",
"@tauri-apps/cli": "^1.2.3",
"classnames": "^2.3.2",
"fast-xml-parser": "4.1.3",
"framer-motion": "^10.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10"
"flag-icons": "^6.6.6",
"solid-icons": "1.0.4",
"solid-js": "^1.7.4"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^3.0.0",
"@types/node": "^18.16.3",
"@walidoux/eslint-config": "1.0.3",
"@walidoux/prettier-config": "1.0.2",
"@walidoux/prettier-config": "1.0.3",
"autoprefixer": "10.4.14",
"eslint": "8.36.0",
"postcss": "8.4.21",
"prettier": "2.8.7",
"prettier-plugin-tailwindcss": "0.2.6",
"tailwindcss": "3.3.1",
"typescript": "^4.7.4",
"vite": "^4.0.0"
"cssnano": "6.0.1",
"cssnano-preset-advanced": "6.0.1",
"eslint": "8.39.0",
"postcss": "8.4.23",
"prettier": "2.8.8",
"prettier-plugin-tailwindcss": "0.2.8",
"tailwindcss": "3.3.2",
"typescript": "^5.0.4",
"vite": "^4.3.4",
"vite-plugin-solid": "^2.7.0",
"vite-plugin-tauri": "3.1.0"
},
"prettier": "@walidoux/prettier-config",
"eslintConfig": {
"extends": [
"@walidoux/eslint-config"
],
"ignorePatterns": [
"**/*.config.js",
"**/*.config.ts"
"**/*.config.cjs"
]
}
}

2143
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

7
postcss.config.cjs Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
...(!process.env.TAURI_DEBUG ? { cssnano: { preset: 'advanced' } } : {})
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

4
prettier.config.cjs Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
...require("@walidoux/prettier-config"),
plugins: ["prettier-plugin-tailwindcss"],
};

Binary file not shown.

BIN
public/icons/cross.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

527
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,13 @@
[package]
name = "rypi-scrapper"
name = "app"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
description = "Scrapping Habbo gamedata and assets"
authors = ["Walidoux"]
license = "MIT"
repository = "https://github.com/RypiDev/rypi-scrapper"
default-run = "app"
edition = "2021"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -13,11 +15,20 @@ edition = "2021"
tauri-build = { version = "1.2", features = [] }
[dependencies]
tauri = { version = "1.2", features = ["fs-create-dir", "fs-exists", "fs-read-dir", "fs-write-file", "http-all", "shell-all", "window-close", "window-maximize", "window-minimize", "window-unmaximize"] }
tauri = { version = "1.2", features = ["fs-create-dir", "fs-exists", "fs-read-dir", "fs-remove-dir", "fs-write-file", "http-all", "path-all", "window-close", "window-minimize", "window-start-dragging"] }
tauri-specta = { version = "1.0.0", features = ["typescript"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
specta = "1.0.3"
quickxml_to_serde = "0.5.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
[profile.release]
strip = true # Automatically strip symbols from the binary
panic = "abort" # Strip expensive panic clean-up logic
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1 @@
{"std":[["li","lh","ls","lc","ri","bd","sh","lg","ch","cc","cp","wa","ca","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","ri","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["bd","sh","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["bd","sh","lg","ch","cc","cp","wa","ca","lh","ls","lc","li","hd","fc","ey","hr","hrb","fa","ea","ha","he","rh","rs","rc","ri"],["rh","rs","rc","ri","bd","sh","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","hd","fc","ey","hr","hrb","fa","ea","ha","he"],["bd","sh","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","hd","fc","ey","hr","hrb","fa","ea","ha","he","rh","rs","rc","ri"],["li","lh","ls","lc","ri","rh","rs","rc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he"]],"lh-up":{"4":["rh","rs","rc","bd","sh","lg","ch","cc","cp","wa","ri","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","lh","ls","lc","li"],"5":["rh","rs","rc","ri","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","li","lh","ls","lc"],"6":["rh","rs","rc","ri","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","li","lh","ls","lc"]},"rh-up":[["li","lh","ls","lc","ri","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","rh","rs","rc"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["li","lh","ls","lc","bd","sh","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"],["bd","sh","lg","ch","cc","cp","wa","li","lh","ls","lc","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc"]],"sit":{"2":["li","lh","ls","lc","bd","lg","ch","cc","cp","wa","ca","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","sh"],"3":["bd","lg","ch","cc","cp","wa","ca","li","lh","ls","lc","rh","rs","rc","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","sh"],"4":["rh","rs","rc","bd","lg","ch","cc","cp","wa","ca","lh","ls","lc","li","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","sh"]},"sit.lh-up":{"4":["rh","rs","rc","bd","lg","ch","cc","cp","wa","ri","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","lh","ls","lc","li","sh"]},"sit.rh-up":{"2":["li","lh","ls","lc","bd","lg","ch","cc","cp","wa","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc","sh"],"3":["bd","lg","ch","cc","cp","wa","li","lh","ls","lc","ca","hd","fc","ey","hr","hrb","fa","ea","ha","he","ri","rh","rs","rc","sh"]},"lay":{"2":["lh","ls","lc","li","bd","lg","sh","ch","cc","cp","hd","fc","ey","wa","ri","rh","rs","rc","ca","hr","hrb","fa","ea","ha","he"],"4":["rh","rs","rc","ri","bd","lg","sh","ch","cc","cp","hd","fc","ey","wa","li","lh","ls","lc","ca","hr","hrb","fa","ea","ha","he"]}}

5
src-tauri/src/build.rs Normal file
View File

@ -0,0 +1,5 @@
use tauri_build::build;
fn main() {
build()
}

112
src-tauri/src/commands.rs Normal file
View File

@ -0,0 +1,112 @@
use std::path::PathBuf;
use quickxml_to_serde::{xml_string_to_json, Config, NullValue};
use specta::specta;
use crate::{
structs::{ConvertTypes, GamedataEndpoints},
utils::convert_json,
};
#[specta]
#[tauri::command]
pub fn download_gamedata(data: &str, endpoint: GamedataEndpoints) {
match endpoint.convert {
ConvertTypes::XML => {
let conf = Config::new_with_custom_values(true, "", "txt", NullValue::Null);
let json = xml_string_to_json(data.to_owned(), &conf);
println!("{}", json.expect("Malformed XML").to_string());
}
ConvertTypes::TXT => println!("TXT CONVERTION"),
ConvertTypes::JSON => convert_json(data, endpoint.file_name),
}
}
#[specta]
#[tauri::command]
pub fn parse_data(
path: &str,
file_name: &str,
file_content: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let file_dir = PathBuf::from(&path).join(format!("{}.json", file_name));
println!("{:?}", file_dir);
// By default, output files will be overwritten. I cannot recursively remove the entire output folder
// and create it again because it just won't parse files' contents for some reason
/* if !exists(&Convertion::gamedata_dir()) {
create_dir_all(&Convertion::gamedata_dir())?;
}
write_file(&file_dir, file_content.as_bytes()); */
Ok(())
}
/* pub async fn convert_txt(path: &str, data: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut badges: Vec<Badge> = Vec::new();
let lines: Vec<&str> = data.split('\n').collect();
let badge_pattern = Regex::new(r"^badge_(?:name|desc)_([^=]+)").unwrap();
let description_pattern = Regex::new(r"^badge_desc_(\s*\w+)").unwrap();
let name_pattern = Regex::new(r"^badge_name_(\s*\w+)").unwrap();
for (index, line) in lines.iter().enumerate() {
let parts: Vec<&str> = line.split('=').collect();
if parts.len() < 2 {
continue;
}
let key = parts[0];
let value = parts[1];
if badge_pattern.is_match(key) {
if name_pattern.is_match(key) {
let badge_code = name_pattern.captures(key).unwrap()[1].to_owned();
let existing_badge = badges.iter_mut().find(|badge| badge.code == badge_code);
if let Some(badge) = existing_badge {
badge.name = Some(value.to_owned());
} else {
badges.push(Badge {
code: badge_code,
name: Some(value.to_owned()),
description: None,
});
}
} else if description_pattern.is_match(key) {
let badge_code = description_pattern.captures(key).unwrap()[1].to_owned();
let existing_badge = badges.iter_mut().find(|badge| badge.code == badge_code);
if let Some(badge) = existing_badge {
badge.description = Some(value.to_owned());
} else {
badges.push(Badge {
code: badge_code,
name: None,
description: Some(value.to_owned()),
});
}
}
// Remove the line from the vector
lines[index] = "";
}
}
// Remove empty lines
let lines: Vec<&str> = lines
.iter()
.filter(|line| !line.is_empty())
.cloned()
.collect();
parse_habbo_data(path, "Badges", &badges);
Ok(())
} */

View File

@ -1,8 +1,28 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
pub mod commands;
pub mod structs;
pub mod utils;
use commands::download_gamedata;
use specta::collect_types;
use tauri_specta::ts;
fn main() {
ts::export(collect_types![download_gamedata], "../src/tools/rusty.ts").unwrap();
// This is useful for custom eslint, prettier overrides at the top of the file.
/* ts::export_with_cfg_with_header(
collect_types![download_gamedata].unwrap(),
Default::default(),
"../src/tools/rusty.ts",
"/* eslint-disable */
\n".into(),
)
.unwrap(); */
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![download_gamedata])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

70
src-tauri/src/structs.rs Normal file
View File

@ -0,0 +1,70 @@
use serde::{Deserialize, Serialize};
use specta::Type;
/* pub struct Badge {
pub code: String,
pub name: Option<String>,
pub description: Option<String>,
} */
#[derive(Serialize, Deserialize, Type, Debug)]
pub enum ConvertTypes {
TXT,
XML,
JSON,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub enum Converters {
FigureData,
FigureMap,
EffectMap,
FurniData,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct GamedataEndpoints {
pub src: String,
pub convert: ConvertTypes,
pub file_name: Converters,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct FurniData {
pub floor_items: Vec<IFloorItem>,
pub wall_items: Vec<IFurni>,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFloorItem {
dimensions: IFloorItemDimensions,
permissions: IFloorItemPermissions,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFloorItemDimensions {
x: i32,
y: i32,
default_direction: i32,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFloorItemPermissions {
can_sit_on: bool,
can_lay_on: bool,
can_stand_on: bool,
}
#[derive(Serialize, Deserialize, Type, Debug)]
pub struct IFurni {
id: i32,
classname: String,
description: Option<String>,
name: Option<String>,
furni_line: Option<String>,
custom_params: Option<String>,
adurl: Option<String>,
offer_id: Option<i32>,
exclude_dynamic: bool,
special_type: i32,
}

54
src-tauri/src/utils.rs Normal file
View File

@ -0,0 +1,54 @@
use serde_json::Value;
use crate::structs::{Converters, FurniData, IFloorItem, IFurni};
pub fn debug_typeof<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}
pub fn convert_json(data: &str, file_name: Converters) {
let object: Value = serde_json::from_str(data).unwrap();
match file_name {
Converters::FurniData => {
let mut data = FurniData {
floor_items: Vec::new(),
wall_items: Vec::new(),
};
let floor_items_result: Result<_, Vec<IFloorItem>> =
Ok(serde_json::from_value::<Vec<IFloorItem>>(
object["wallitemtypes"]["furnitype"].clone(),
));
let wall_items_result: Result<_, Vec<IFurni>> = Ok(
serde_json::from_value::<Vec<IFurni>>(object["roomitemtypes"]["furnitype"].clone()),
);
// Handle the floor_items_result and wall_items_result appropriately
match (floor_items_result, wall_items_result) {
(Ok(floor_items), Ok(wall_items)) => {
while let Ok(ref value) = wall_items {
println!("{:?}", value);
}
while let Ok(ref value) = floor_items {
println!("{:?}", value);
}
}
(Err(e), _) => {
println!("Error deserializing floor items: {:?}", e);
}
(_, Err(e)) => {
println!("Error deserializing wall items: {:?}", e);
}
}
// loop through each key value pairs (make sure your handle null exceptions)
// parse the data to the global variable above
//
}
Converters::FigureData => {}
Converters::FigureMap => {}
Converters::EffectMap => {}
}
}

View File

@ -1,11 +1,5 @@
{
"build": {
"beforeDevCommand": "yarn dev",
"beforeBuildCommand": "yarn build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "rypi-scrapper",
"version": "0.0.0"
@ -15,8 +9,7 @@
"window": {
"close": true,
"minimize": true,
"maximize": true,
"unmaximize": true
"startDragging": true
},
"os": {
"all": false
@ -25,21 +18,12 @@
"createDir": true,
"exists": true,
"readDir": true,
"removeDir": true,
"writeFile": true,
"scope": ["$DOWNLOAD/**/*"]
},
"shell": {
"all": true,
"execute": true,
"sidecar": true,
"open": true,
"scope": [
{
"name": "download-habbo-downloader",
"cmd": "npm",
"args": ["install", "-g", "habbo-downloader"]
}
]
"path": {
"all": true
},
"http": {
"all": true,
@ -49,18 +33,12 @@
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.tauri.dev",
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"identifier": "org.rypi.dev",
"targets": "all"
},
"security": {
"csp": null
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
},
"updater": {
"active": false
@ -68,10 +46,10 @@
"windows": [
{
"decorations": false,
"resizable": false,
"center": true,
"resizable": true,
"width": 800,
"height": 500
"width": 1000,
"height": 575
}
]
}

View File

@ -1,52 +1,20 @@
import { useState } from 'react'
import type { Component } from 'solid-js'
import { Image, Window, Button, Downloader } from './components'
import { fetchGameData } from './utils/fetchGameData'
import {
GameAssetsDownloader,
GameDataDownloader
} from './config/GameDownloader'
const Main: React.FC = () => {
const [error, setError] = useState('')
const [response, setResponse] = useState('')
const [loading, setLoading] = useState(false)
const callback = (message: string, error = false): void => {
if (error) return setError(message)
if (message === 'COMPLETED') setLoading(false)
else setResponse(message)
}
import { Downloaders, TitleBar, LangDropdown } from './components/layout'
const Main: Component = () => {
return (
<Window>
<span className='mb-20 text-white'>I would like to:</span>
<>
<TitleBar />
<ul className='flex gap-x-8'>
<Downloader content={GameDataDownloader}>
<Image src='/images/Gamedata.png' className='w-[400px]' />
<main class='relative flex h-full w-screen flex-col items-center justify-center bg-[#242424] py-20 text-white'>
<span class='mb-20'>I would like to:</span>
<Button
value='Download Gamedata'
icon={<Image src='/icons/game.png' size={22} />}
className='download-button border-gamedata-secondary bg-gamedata-primary shadow-gamedata-primary/20'
handler={async () => {
return await fetchGameData('com', callback)
}}
/>
</Downloader>
<LangDropdown />
<Downloader content={GameAssetsDownloader}>
<Image src='/images/GameAssets.png' className='w-[400px]' />
<Button
value='Download GameAssets'
icon={<Image src='/icons/picture.png' icon />}
className='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40'
/>
</Downloader>
</ul>
</Window>
<Downloaders />
</main>
</>
)
}

View File

@ -1,27 +0,0 @@
import classNames from 'classnames'
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
icon: JSX.Element
value: string
className?: string
handler?: () => void
}
export const Button: React.FC<ButtonProps> = ({
icon,
value,
className,
handler
}) => {
return (
<button
onClick={handler}
className={classNames(
className,
'flex items-center justify-center gap-x-5'
)}>
{icon}
<span className='uppercase'>{value}</span>
</button>
)
}

View File

@ -1,68 +0,0 @@
import classNames from 'classnames'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
export interface IDownloaderContent {
title: string
features: string[]
}
interface DownloaderProps {
className?: string
children: React.ReactNode
content: IDownloaderContent
}
export const Downloader: React.FC<DownloaderProps> = ({
className,
children,
content
}) => {
const [activeContent, setActiveContent] = useState(false)
const handleActiveContent = (): void => {
return setActiveContent(!activeContent)
}
return (
<li
className={classNames(
className,
'relative rounded-xl shadow-red-500 hover:shadow-2xl'
)}
onMouseEnter={handleActiveContent}
onMouseLeave={handleActiveContent}>
{children}
<AnimatePresence>
{activeContent && (
<motion.ul
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { staggerChildren: 0.5 } }}
exit={{ opacity: 0 }}
className='pointer-events-none absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center rounded-xl bg-black/70 pb-5 text-[12px] text-white'>
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className='mb-3 text-[16px]'>
{content.title}
</motion.h1>
{content.features.map((feature) => {
return (
<motion.span
initial={{ opacity: 0, y: 50, scale: 0.75 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 50, scale: 0.75 }}
key={feature}>
{feature}
</motion.span>
)
})}
</motion.ul>
)}
</AnimatePresence>
</li>
)
}

View File

@ -1,19 +0,0 @@
import classNames from 'classnames'
interface ImageProps extends React.ComponentPropsWithoutRef<'img'> {
size?: number
icon?: boolean
}
export const Image: React.FC<ImageProps> = ({ size, icon, ...rest }) => {
return (
<img
{...rest}
draggable={false}
style={{ imageRendering: Boolean(icon) ? 'pixelated' : 'unset' }}
className={classNames(rest.className, 'select-none')}
height={size ?? rest.height}
width={size ?? rest.width}
/>
)
}

View File

@ -1,38 +0,0 @@
import { Maximize, Minus, X } from 'react-feather'
import { appWindow } from '@tauri-apps/api/window'
import { Image } from '../Image'
export const TitleBar: React.FC = () => {
return (
<nav className='flex h-[37.5px] items-center justify-between bg-[#1f1f1f] text-white'>
<Image src='/Logo.svg' size={60} className='ml-3 p-1' />
<ul className='flex h-full'>
<li
onClick={async () => {
return await appWindow.minimize()
}}
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-[#2a2a2a]'>
<Minus />
</li>
<li
onClick={async () => {
return (await appWindow.isMaximized())
? await appWindow.unmaximize()
: await appWindow.maximize()
}}
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-[#2a2a2a]'>
<Maximize size={20} />
</li>
<li
onClick={async () => {
return await appWindow.close()
}}
className='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-red-500'>
<X />
</li>
</ul>
</nav>
)
}

View File

@ -1,19 +0,0 @@
import { Fragment } from 'react'
import { TitleBar } from '../TitleBar/TitleBar'
interface WindowProps {
children: React.ReactNode
}
export const Window: React.FC<WindowProps> = ({ children }) => {
return (
<Fragment>
<TitleBar />
<main className='flex h-screen w-screen flex-col items-center justify-center overflow-hidden bg-[#242424] p-10'>
{children}
</main>
</Fragment>
)
}

View File

@ -1 +0,0 @@
export * from './Window'

View File

@ -0,0 +1,22 @@
import { Show } from 'solid-js'
import type { Component } from 'solid-js'
import type { MotionComponentProps, Variant } from '@motionone/solid'
import { Motion, Presence } from '@motionone/solid'
export interface AnimateViewProps extends MotionComponentProps {
animation?: Record<string, Variant>
condition: boolean
class?: string
}
export const AnimateView: Component<AnimateViewProps> = (props) => {
return (
<Presence>
<Show when={props.condition}>
<Motion.div class={props.class} {...props.animation}>
{props.children}
</Motion.div>
</Show>
</Presence>
)
}

View File

@ -0,0 +1 @@
export * from './AnimateView'

View File

@ -0,0 +1,18 @@
import classNames from 'classnames'
import type { Component, ComponentProps, JSXElement } from 'solid-js'
interface ButtonProps extends ComponentProps<'button'> {
icon: JSXElement
value: string
class?: string
handler?: () => void
}
export const Button: Component<ButtonProps> = (props) => {
return (
<button onClick={props.handler} class={classNames(props.class, 'flex items-center justify-center gap-x-5')}>
{props.icon}
<span class='uppercase'>{props.value}</span>
</button>
)
}

View File

@ -0,0 +1,22 @@
import classNames from 'classnames'
import type { Component } from 'solid-js'
import { SUPPORTED_LANGS } from '../../../config'
interface FlagProps {
label?: boolean
domain: string
}
export const Flag: Component<FlagProps> = (props) => {
const lang = SUPPORTED_LANGS.filter((lang) => {
return lang.domain === props.domain
})[0]
return (
<>
<span class={classNames('', `fi fi-${lang.code}`)} />
{Boolean(props.label) && <span class='ml-3'>{lang.name}</span>}
</>
)
}

View File

@ -0,0 +1 @@
export * from './Flag'

View File

@ -0,0 +1,25 @@
import classNames from 'classnames'
import type { Component } from 'solid-js'
interface ImageProps {
size?: number
pixelated?: boolean
src: string
class?: string
height?: number
width?: number
}
export const Image: Component<ImageProps> = (props) => {
return (
<img
{...props}
elementtiming=''
fetchpriority='auto'
style={{ 'image-rendering': Boolean(props.pixelated) ? 'pixelated' : 'unset' }}
class={classNames(props.class, 'select-none')}
height={props.size ?? props.height}
width={props.size ?? props.width}
/>
)
}

View File

@ -0,0 +1,67 @@
import type { Component } from 'solid-js'
import { Animation } from '../../../config'
import { AnimateView } from '../AnimateView'
interface LoaderProps {
class?: string
active: boolean
}
export const Loader: Component<LoaderProps> = (props) => {
return (
<AnimateView
class={props.class}
condition={props.active}
variants={Animation.fadeInOut({ scale: [0, 1, 0], y: [1, 4, 1] })}>
<svg width='44' height='44' viewBox='0 0 44 44' xmlns='http://www.w3.org/2000/svg' stroke='#fff'>
<g fill='none' fill-rule='evenodd' stroke-width={2}>
<circle cx='22' cy='22' r='1'>
<animate
attributeName='r'
begin='0s'
dur='1.8s'
values='1; 20'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.165, 0.84, 0.44, 1'
repeatCount='indefinite'
/>
<animate
attributeName='stroke-opacity'
begin='0s'
dur='1.8s'
values='1; 0'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.3, 0.61, 0.355, 1'
repeatCount='indefinite'
/>
</circle>
<circle cx='22' cy='22' r='1'>
<animate
attributeName='r'
begin='-0.9s'
dur='1.8s'
values='1; 20'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.165, 0.84, 0.44, 1'
repeatCount='indefinite'
/>
<animate
attributeName='stroke-opacity'
begin='-0.9s'
dur='1.8s'
values='1; 0'
calcMode='spline'
keyTimes='0; 1'
keySplines='0.3, 0.61, 0.355, 1'
repeatCount='indefinite'
/>
</circle>
</g>
</svg>
</AnimateView>
)
}

View File

@ -0,0 +1 @@
export * from './Loader'

View File

@ -0,0 +1,5 @@
export * from './Loader'
export * from './AnimateView'
export * from './Image'
export * from './Button'
export * from './Flag'

View File

@ -1,4 +0,0 @@
export * from './Window'
export * from './Image'
export * from './Button'
export * from './Downloader'

View File

@ -0,0 +1,48 @@
import { Motion } from '@motionone/solid'
import classNames from 'classnames'
import type { Component, JSXElement } from 'solid-js'
import { For, createSignal } from 'solid-js'
import type { GameDataDownloader } from '../../../../config'
import { Animation } from '../../../../config'
import { AnimateView } from '../../../design'
interface DownloaderProps {
className?: string
children: JSXElement
content: typeof GameDataDownloader
}
export const Downloader: Component<DownloaderProps> = (props) => {
const [activeContent, setActiveContent] = createSignal(false)
const handleActiveContent = (): boolean => {
return setActiveContent(!activeContent())
}
return (
<li
class={classNames(props.className, 'relative rounded-xl shadow-red-500 hover:shadow-2xl')}
onMouseEnter={handleActiveContent}
onMouseLeave={handleActiveContent}>
{props.children}
<AnimateView
animation={Animation.fadeInOut()}
condition={activeContent()}
class='pointer-events-none absolute left-0 top-0 flex h-full w-full flex-col items-center justify-center rounded-xl bg-black/70 pb-5 text-center text-[12px] text-white'>
<Motion.h1 {...Animation.fadeInOut({ y: [-20, 0, -20] })} class='mb-3 text-[16px]'>
{props.content.title}
</Motion.h1>
<For each={props.content.features}>
{(feature) => {
return (
<Motion.span {...Animation.fadeInOut({ y: [50, 0, 50], scale: [0.75, 1, 0.75] })}>{feature}</Motion.span>
)
}}
</For>
</AnimateView>
</li>
)
}

View File

@ -0,0 +1,96 @@
import type { Component } from 'solid-js'
import { createSignal } from 'solid-js'
import classNames from 'classnames'
import type { ConvertionHandler } from '../../../types'
import { handleConvertion } from '../../../tools/handleConvertion'
import { GameAssetsDownloader, GameDataDownloader } from '../../../config'
import { Button, Image, Loader } from '../../design'
import { Popup } from '../Popup'
import { Downloader } from './Downloader/Downloader'
export const Downloaders: Component = () => {
const [message, setMessage] = createSignal('')
const [error, setError] = createSignal(false)
const [popup, setPopup] = createSignal(false)
const [loading, setLoading] = createSignal(false)
const callback: ConvertionHandler = (message, state = 'idle') => {
switch (state) {
case 'loading':
setMessage(message)
return setLoading(true)
case 'success':
setMessage(message)
return setLoading(false)
case 'error':
setMessage(message)
setError(true)
return setLoading(false)
}
}
const downloadGameData = async (): Promise<void> => {
setPopup(true)
setLoading(true)
const startTime = new Date()
await handleConvertion('com', callback)
const endTime = new Date()
const seconds = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2)
return callback(`Completed in: ${seconds} seconds`, 'success')
}
return (
<>
<Popup condition={popup()}>
<span class={classNames('', { 'text-red-600': error })}>{message()}</span>
<Loader active={loading()} class='mt-10' />
<Button
value='Abort'
icon={<Image src='/icons/cross.png' />}
class='mt-6 bg-red-600 p-2 px-4 active:opacity-40'
handler={() => {}}
/>
<Button
value='Close'
icon={<Image src='/icons/cross.png' />}
class={classNames('invisible mt-6 bg-red-600 p-2 px-4 opacity-0 active:opacity-40', {
'!visible !opacity-100': !loading()
})}
handler={() => {
return setPopup(!popup())
}}
/>
</Popup>
<ul class='flex gap-x-8'>
<Downloader content={GameDataDownloader}>
<Image src='/images/Gamedata.png' />
<Button
value='Download Gamedata'
icon={<Image src='/icons/game.png' size={22} />}
class='download-button border-gamedata-secondary bg-gamedata-primary shadow-gamedata-primary/20'
handler={downloadGameData}
/>
</Downloader>
<Downloader content={GameAssetsDownloader}>
<Image src='/images/GameAssets.png' />
<Button
value='Download GameAssets'
icon={<Image src='/icons/picture.png' pixelated />}
class='download-button border-gameAssets-secondary bg-gameAssets-primary shadow-gameAssets-primary/40'
/>
</Downloader>
</ul>
</>
)
}

View File

@ -0,0 +1 @@
export * from './Downloaders'

View File

@ -0,0 +1,51 @@
import type { Component } from 'solid-js'
import { createSignal, For } from 'solid-js'
import { Motion } from '@motionone/solid'
import { useLocalStorage } from '../../../hooks/useLocalStorage'
import { AnimateView, Flag } from '../../design'
import { OutSideEventHandler } from '../OutSideEventHandler'
import { Animation, SUPPORTED_LANGS } from '../../../config'
export const LangDropdown: Component = () => {
const { lang, setLang } = useLocalStorage()
let ref: HTMLDivElement | undefined
const [active, setActive] = createSignal(false)
return (
<OutSideEventHandler
class='relative text-sm'
onOutsideClick={() => {
return setActive(false)
}}>
<div
ref={ref}
class='cursor-pointer'
onclick={() => {
return setActive(!active())
}}>
Selected Lang: <Flag domain={lang()} />
</div>
<AnimateView condition={active()} class='absolute top-8 z-20' animation={Animation.fadeInOut()}>
<For each={SUPPORTED_LANGS}>
{(lang, index) => {
return (
<Motion.p
{...Animation.fadeInOut({ x: [50 / (index() + 1), 0, 0] })}
transition={{ delay: (index() + 1) / 15 }}
class='cursor-pointer'
onclick={() => {
setActive(false)
return setLang(lang.domain)
}}>
<Flag domain={lang.domain} label />
</Motion.p>
)
}}
</For>
</AnimateView>
</OutSideEventHandler>
)
}

View File

@ -0,0 +1 @@
export * from './LangDropdown'

View File

@ -0,0 +1,35 @@
import type { Component, ComponentProps } from 'solid-js'
import { onCleanup, onMount } from 'solid-js'
interface OutSideEventHandlerProps extends ComponentProps<'div'> {
onOutsideClick: () => void
}
export const OutSideEventHandler: Component<OutSideEventHandlerProps> = (props) => {
let ref: HTMLDivElement | undefined
const handleClickOutside = (event: MouseEvent | KeyboardEvent): void => {
const currentEvent = event as KeyboardEvent
if (ref == null) return
if (ref.contains(event.target as Node) || currentEvent.key !== 'Escape') return
return props.onOutsideClick()
}
onMount(() => {
window.addEventListener('keydown', handleClickOutside)
return window.addEventListener('click', handleClickOutside)
})
onCleanup(() => {
window.removeEventListener('keydown', handleClickOutside)
return window.removeEventListener('click', handleClickOutside)
})
return (
<div {...props} ref={ref}>
{props.children}
</div>
)
}

View File

@ -0,0 +1 @@
export * from './OutSideEventHandler'

View File

@ -0,0 +1,20 @@
import type { Component, JSXElement } from 'solid-js'
import { AnimateView } from '../../design'
import { Animation } from '../../../config'
interface PopupProps {
condition: boolean
children: JSXElement
}
export const Popup: Component<PopupProps> = (props) => {
return (
<AnimateView
animation={Animation.fadeInOut()}
class='absolute left-0 top-0 z-20 flex h-screen w-screen flex-col items-center justify-center bg-black/40'
condition={props.condition}>
{props.children}
</AnimateView>
)
}

View File

@ -0,0 +1 @@
export * from './Popup'

View File

@ -0,0 +1,41 @@
import { FiMaximize, FiMinus, FiX } from 'solid-icons/fi'
import { appWindow } from '@tauri-apps/api/window'
import type { Component } from 'solid-js'
import { Image } from '../../design'
export const TitleBar: Component = () => {
return (
<nav class='!z-50 flex h-[40px] w-full items-center justify-between bg-[#1f1f1f] text-white'>
<div
class='w-full select-none'
onMouseDown={async () => {
return await appWindow.startDragging()
}}>
<Image src='/Logo.svg' size={60} class='ml-3 p-1' />
</div>
<ul class='flex h-full'>
<li
class='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-[#2a2a2a]'
onClick={async () => {
return await appWindow.minimize()
}}>
<FiMinus />
</li>
<li class='grid h-full w-14 cursor-not-allowed place-items-center opacity-40'>
<FiMaximize size={20} />
</li>
<li
class='grid h-full w-14 cursor-pointer place-items-center transition-colors duration-[10ms] hover:bg-red-500'
onClick={async () => {
return await appWindow.close()
}}>
<FiX />
</li>
</ul>
</nav>
)
}

View File

@ -0,0 +1,5 @@
export * from './Downloaders'
export * from './Popup'
export * from './TitleBar'
export * from './LangDropdown'
export * from './OutSideEventHandler'

23
src/config/Animation.ts Normal file
View File

@ -0,0 +1,23 @@
import type { Variant } from '@motionone/solid'
const fadeInOut = (variant?: Variant): Record<string, Variant> => {
const getVariantAtPosition = (pos: 0 | 1 | 2): Record<string, number> | null => {
if (variant == null) return null
const variantAtPos: Record<string, number> = {}
for (const [key, value] of Object.entries(variant)) {
variantAtPos[key] = (value as number[])[pos as keyof typeof value]
}
return variantAtPos
}
return {
initial: { opacity: 0, ...getVariantAtPosition(0) },
animate: { opacity: 1, ...getVariantAtPosition(1) },
exit: { opacity: 0, ...getVariantAtPosition(2) }
}
}
export const Animation = { fadeInOut }

10
src/config/Convertion.ts Normal file
View File

@ -0,0 +1,10 @@
import { downloadDir } from '@tauri-apps/api/path'
import { PROD_VERSION } from './Endpoints'
const outputDir = (await downloadDir()).concat(String(PROD_VERSION))
const gamedataDir = outputDir.concat('/gamedata')
const genericDir = outputDir.concat('/generic')
export const Convertion = { outputDir, gamedataDir, genericDir }

13
src/config/Domain.ts Normal file
View File

@ -0,0 +1,13 @@
// ISO 3166-1-alpha-2 Flags
export const SUPPORTED_LANGS = [
{ name: 'Portuguese', code: 'br', domain: 'com.br' },
{ name: 'Turkish', code: 'tr', domain: 'com.tr' },
{ name: 'English', code: 'us', domain: 'com' },
{ name: 'German', code: 'de', domain: 'de' },
{ name: 'Spanish', code: 'es', domain: 'es' },
{ name: 'Finnish', code: 'fi', domain: 'fi' },
{ name: 'French', code: 'fr', domain: 'fr' },
{ name: 'Italian', code: 'it', domain: 'it' },
{ name: 'Dutch', code: 'nl', domain: 'nl' }
]

View File

@ -1,62 +0,0 @@
import { getClient, ResponseType } from '@tauri-apps/api/http'
import type { DomainTypes, GameEndPointsTypes } from '../types'
const PROD_VERSION_REGEX = /(production-[^/]+)/im
const STABLE_PROD_VERSION = 'PRODUCTION-202303282207-162719871'
export let PROD_VERSION: string | undefined
const HABBO_URL = (domain: DomainTypes): string => {
return `https://www.habbo.${domain}`
}
const HABBO_IMAGES = `https://images.habbo.com/gordon/${
PROD_VERSION ?? STABLE_PROD_VERSION
}`
export const client = await getClient()
await client
.get(`${HABBO_URL('com')}/gamedata/external_variables/0`, {
responseType: ResponseType.Text
})
.then(({ data }) => {
return (PROD_VERSION = (data as string).match(PROD_VERSION_REGEX)?.[0])
})
export const GAME_ENDPOINTS: GameEndPointsTypes = (domain) => {
return [
{
src: `${HABBO_URL(domain)}/gamedata/figuredata/0`,
convert: 'XML',
fileName: 'FigureData'
}
/* {
src: `${HABBO_IMAGES}/figuremap.xml`,
convert: 'XML',
fileName: 'FigureMap'
},
{
src: `${HABBO_URL(domain)}/gamedata/furnidata_json/0`,
fileName: 'FurniData'
},
{
src: `${HABBO_URL(domain)}/gamedata/productdata_json/0`,
fileName: 'ProductData'
},
{
src: `${HABBO_IMAGES}/effectmap.xml`,
convert: 'XML',
fileName: 'EffectMap'
},
{
src: `${HABBO_URL(domain)}/gamedata/external_variables/0`,
convert: 'TXT',
fileName: 'ExternalTexts'
} */
]
}
export const ASSETS_ENDPOINTS = (domain: DomainTypes): string[] => {
return [`${HABBO_URL(domain)}/`]
}

39
src/config/Endpoints.ts Normal file
View File

@ -0,0 +1,39 @@
import { getClient, ResponseType } from '@tauri-apps/api/http'
import type { GamedataEndpoints } from '../tools/rusty'
import { useLocalStorage } from '../hooks/useLocalStorage'
const { lang } = useLocalStorage()
export const client = await getClient()
const PROD_VERSION_REGEX = /(production-[^/]+)/im
const STABLE_PROD_VERSION = 'PRODUCTION-202304181630-471782382'
export let PROD_VERSION: string | undefined
const HABBO_URL = (domain: string): string => {
return `https://www.habbo.${domain}`
}
export const HABBO_GORDON_URL = `https://images.habbo.com/gordon/${PROD_VERSION ?? STABLE_PROD_VERSION}`
export const GAMEDATA_ENDPOINTS = async (domain: string): Promise<GamedataEndpoints[]> => {
return [
{
src: `${HABBO_URL(domain)}/gamedata/furnidata_json/0`,
convert: 'JSON',
file_name: 'FurniData'
}
]
}
export const ASSETS_ENDPOINTS = (domain: string): string[] => {
return [`${HABBO_URL(domain)}/`]
}
await client
.get(`${HABBO_URL(lang())}/gamedata/external_variables/0`, {
responseType: ResponseType.Text
})
.then(({ data }) => {
return (PROD_VERSION = (data as string).match(PROD_VERSION_REGEX)?.[0])
})

View File

@ -1,17 +1,15 @@
import type { IDownloaderContent } from '../components'
export const GameDataDownloader: IDownloaderContent = {
export const GameDataDownloader = {
title: 'Converts and bundles:',
features: ['XML/TXT to minified JSON files', 'Converts SWF files to JSV']
features: ['XML/TXT to minified JSON files', 'Converts SWF files to Sprite']
}
export const GameAssetsDownloader: IDownloaderContent = {
export const GameAssetsDownloader = {
title: 'Fetches PNG/JPEG:',
features: [
'Badges + Badgeparts',
'Album + Recepetion images',
'Catalogue + Furni icons',
'Habbo Web Promo + Articles',
'MP3 Sounds machine'
'MP3 Sounds'
]
}

5
src/config/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './Animation'
export * from './Convertion'
export * from './Domain'
export * from './Endpoints'
export * from './GameDownloader'

View File

@ -0,0 +1,19 @@
import type { IEffectMap } from '../types'
export class EffectMap {
public data: IEffectMap = {}
public fileName: string
constructor(XML: any, fileName: string) {
this.fileName = fileName
this.parseLibrairies(XML.map.effect)
}
private parseLibrairies(effects: any[]): void {
for (const libraryXML of effects) {
this.data[libraryXML.type] == null && (this.data[libraryXML.type] = {})
this.data[libraryXML.type][libraryXML.id] = libraryXML.lib
}
}
}

View File

@ -0,0 +1,78 @@
import type { IFigureData, IFigureDataPalette, IFigureDataSet, IFigureDataSetType, IXML } from '../types'
export class FigureData {
public data: IFigureData = { palettes: [], setTypes: {} }
public fileName: string
constructor(XML: IXML, fileName: string) {
this.fileName = fileName
this.parsePalettes(XML.figuredata.colors.palette)
this.parseSetTypes(XML.figuredata.sets.settype)
}
private parsePalettes(palettes: any[]): void {
for (const paletteXML of palettes) {
const palette = {} as IFigureDataPalette
for (const colorXML of paletteXML.color) {
palette[Number(colorXML.id)] = {
index: Number(colorXML.index),
club: Number(colorXML.club),
selectable: Boolean(colorXML.selectable),
color: '#' + String(colorXML['#text'])
}
}
this.data.palettes.push(palette)
}
}
private parseSetTypes(setTypes: any[]): void {
for (const setTypeXML of setTypes) {
const settype: IFigureDataSetType = {
paletteId: Number(setTypeXML.paletteid),
mandatoryF0: Boolean(Number(setTypeXML.mand_f_0)),
mandatoryF1: Boolean(Number(setTypeXML.mand_f_1)),
mandatoryM0: Boolean(Number(setTypeXML.mand_m_0)),
mandatoryM1: Boolean(Number(setTypeXML.mand_m_1)),
sets: {}
}
for (const setXML of setTypeXML.set) {
const setType: IFigureDataSet = {
gender: setXML.gender,
club: Number(setXML.club),
colorable: Boolean(Number(setXML.colorable)),
selectable: Boolean(Number(setXML.selectable)),
preselectable: Boolean(Number(setXML.preselectable)),
sellable: setXML.sellable != null ? Boolean(Number(setXML.sellable)) : undefined,
parts: [],
hiddenLayers: []
}
for (const partXML of Array.isArray(setXML.part) ? setXML.part : [setXML.part]) {
setType.parts.push({
id: Number(partXML.id),
type: partXML.type,
colorable: Boolean(Number(partXML.colorable)),
index: Number(partXML.index),
colorindex: Number(partXML.colorindex)
})
}
if (setXML.hiddenLayers != null) {
for (const hiddenLayerXML of Array.isArray(setXML.hiddenLayers)
? setXML.hiddenLayers
: [setXML.hiddenLayers]) {
setType.hiddenLayers?.push(hiddenLayerXML.partType)
}
}
settype.sets[Number(setXML.id)] = setType
}
this.data.setTypes[String(setTypeXML.type)] = settype
}
}
}

View File

@ -0,0 +1,25 @@
import type { IFigureMap, IFigureMapLibrary, IXML } from '../types'
export class FigureMap {
public data: IFigureMap = { libraries: [], parts: {} }
public fileName: string
constructor(XML: IXML, fileName: string) {
this.fileName = fileName
this.parseLibrairies(XML.map.lib)
}
private parseLibrairies(librairies: any[]): void {
for (const libraryXML of librairies) {
const library: IFigureMapLibrary = { id: libraryXML.id, revision: Number(libraryXML.revision) }
for (const libraryPart of Array.isArray(libraryXML.part) ? libraryXML.part : [libraryXML.part]) {
this.data.parts[libraryPart.type] == null && (this.data.parts[libraryPart.type] = {})
this.data.parts[libraryPart.type][Number(libraryPart.id)] = librairies.indexOf(libraryXML)
}
this.data.libraries.push(library)
}
}
}

View File

@ -0,0 +1,18 @@
import type { Accessor } from 'solid-js'
import { createSignal } from 'solid-js'
export interface ILocalStorage {
lang: Accessor<string>
setLang: (domain: string) => void
}
const [lang, setLangeState] = createSignal<string>((localStorage.getItem('LANG_KEY') as string) ?? 'com')
const setLang: ILocalStorage['setLang'] = (domain) => {
localStorage.setItem('LANG_KEY', domain)
return setLangeState(domain)
}
export const useLocalStorage = (): ILocalStorage => {
return { lang, setLang }
}

11
src/index.tsx Normal file
View File

@ -0,0 +1,11 @@
import { render } from 'solid-js/web'
import './styles/fonts.css'
import './styles/styles.css'
import 'flag-icons/css/flag-icons.min.css'
import App from './App'
render(() => {
return <App />
}, document.getElementById('root') as HTMLElement)

View File

@ -1,12 +0,0 @@
import React from 'react'
import { createRoot } from 'react-dom/client'
import './styles.css'
import '@fontsource/press-start-2p'
import App from './App'
createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@ -1,3 +0,0 @@
export const convertTXT = (data: string) => {
console.log(data)
}

View File

@ -1,194 +0,0 @@
import type {
IEffectMap,
IEffectMapLibrary,
IFigureData,
IFigureDataColor,
IFigureDataHiddenLayer,
IFigureDataPalette,
IFigureDataPart,
IFigureDataSet,
IFigureDataSetType,
IFigureMap,
IFigureMapLibrary,
IFigureMapLibraryPart
} from '../types'
export const convertXML = (
XML: any,
url: string
): IFigureData | IFigureMap | IEffectMap => {
if (url.includes('figuredata')) {
const output: IFigureData = { palettes: [], setTypes: [] }
for (const paletteXML of XML.figuredata.colors.palette) {
const palette: IFigureDataPalette = { id: 0, color: [] }
if (paletteXML.id !== undefined) palette.id = paletteXML.id
if (paletteXML.color !== undefined) {
for (const colorXML of paletteXML.color) {
const color: IFigureDataColor = {
id: 0,
index: 0,
club: 0,
selectable: false,
hexCode: ''
}
const hexColor = String(colorXML['#text' as keyof IFigureDataColor])
if (colorXML.id !== undefined) color.id = colorXML.id
if (colorXML.index !== undefined) color.index = colorXML.index
if (colorXML.club !== undefined) color.club = colorXML.club
if (colorXML.selectable !== undefined)
color.selectable = colorXML.selectable === 0 ? true : false
if (hexColor !== undefined) color.hexCode = hexColor
palette.color.push(color)
}
}
output.palettes.push(palette)
}
for (const setTypeXML of XML.figuredata.sets.settype) {
const settype: IFigureDataSetType = {
type: '',
paletteId: 0,
mandatoryF0: false,
mandatoryF1: false,
mandatoryM0: false,
mandatoryM1: false,
sets: []
}
if (setTypeXML.type !== undefined) settype.type = setTypeXML.type
if (setTypeXML.paletteId !== undefined)
settype.paletteId = setTypeXML.paletteId
if (setTypeXML.mandatoryF0 !== undefined)
settype.mandatoryF0 = setTypeXML.mandatoryF0
if (setTypeXML.mandatoryF1 !== undefined)
settype.mandatoryF1 = setTypeXML.mandatoryF1
if (setTypeXML.mandatoryM0 !== undefined)
settype.mandatoryM0 = setTypeXML.mandatoryM0
if (setTypeXML.mandatoryM1 !== undefined)
settype.mandatoryM1 = setTypeXML.mandatoryM1
if (setTypeXML.sets !== undefined) {
for (const setXML of setTypeXML.sets) {
const setType: IFigureDataSet = {
id: 0,
gender: 'U',
club: 0,
colorable: false,
selectable: false,
preselectable: false,
sellable: false,
parts: [],
hiddenLayers: []
}
if (setXML.id !== undefined) setType.id = setXML.id
if (setXML.gender !== undefined) setType.gender = setXML.gender
if (setXML.club !== undefined) setType.club = setXML.club
if (setXML.colorable !== undefined)
setType.colorable = setXML.colorable === 0 ? true : false
if (setXML.selectable !== undefined)
setType.selectable = setXML.selectable
if (setXML.preselectable !== undefined)
setType.preselectable = setXML.preselectable
if (setXML.sellable !== undefined) setType.sellable = setXML.sellable
if (setXML.parts !== undefined) {
for (const partXML of setXML.parts) {
const part: IFigureDataPart = {
id: 0,
type: '',
colorable: false,
index: 0,
colorindex: 0
}
if (partXML.id !== undefined) part.id = partXML.id
if (partXML.type !== undefined) part.type = partXML.type
if (partXML.colorable !== undefined)
part.colorable = partXML.colorable
if (partXML.index !== undefined) part.index = partXML.index
if (partXML.colorindex !== undefined)
part.colorindex = partXML.colorindex
setType.parts.push(part)
}
}
if (setXML.hiddenLayers !== undefined) {
for (const hiddenLayerXML of setXML.hiddenLayers) {
const hiddenLayer: IFigureDataHiddenLayer = { partType: '' }
if (hiddenLayerXML.partType !== undefined)
hiddenLayer.partType = hiddenLayerXML.partType
setType.hiddenLayers?.push(hiddenLayer)
}
}
settype.sets.push(setType)
}
}
output.setTypes.push(setTypeXML)
}
return output
} else if (url.includes('figuremap')) {
const output: IFigureMap = { libraries: [] }
for (const libraryXML of XML.map.lib as IFigureMapLibrary[]) {
const library: IFigureMapLibrary = { id: '', revision: 0, part: [] }
if (libraryXML.id !== undefined) library.id = libraryXML.id
if (libraryXML.revision !== undefined)
library.revision = libraryXML.revision
if (Array.isArray(libraryXML.part)) {
for (const libraryPart of libraryXML.part) {
const libraryPartXML: IFigureMapLibraryPart = { id: 0, type: '' }
if (libraryPart.id !== undefined) libraryPartXML.id = libraryPart.id
if (libraryPart.type !== undefined)
libraryPartXML.type = libraryPart.type
library.part.push(libraryPartXML)
}
} else {
const libraryPart = libraryXML.part as unknown as IFigureMapLibraryPart
library.part.push({ id: libraryPart.id, type: libraryPart.type })
}
output.libraries.push(library)
}
return output
} else {
const output: IEffectMap = { effects: [] }
for (const libraryXML of XML.map.effect as IEffectMapLibrary[]) {
const library: IEffectMapLibrary = {
id: 0,
lib: '',
type: '',
revision: 0
}
if (libraryXML.id !== undefined) library.id = libraryXML.id
if (libraryXML.lib !== undefined) library.lib = libraryXML.lib
if (libraryXML.type !== undefined) library.type = libraryXML.type
if (libraryXML.revision !== undefined)
library.revision = libraryXML.revision
output.effects.push(library)
}
return output
}
}

View File

@ -1,2 +0,0 @@
export * from './convertXML'
export * from './convertTXT'

13
src/styles/fonts.css Normal file
View File

@ -0,0 +1,13 @@
@font-face {
font-family: 'Press Start 2P';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('./PressStart2P-Regular.ttf');
}
#root {
font-family: 'Press Start 2P';
overflow: hidden;
height: 100vh;
}

View File

@ -2,10 +2,6 @@
@tailwind components;
@tailwind utilities;
#root {
font-family: 'Press Start 2P';
}
@layer utilities {
.retro-text-shadow {
text-shadow: 0px 1.5px 0px #000000, -1px 3px 0px rgba(0, 0, 0, 0.54);
@ -20,6 +16,6 @@
@layer components {
.download-button {
@apply retro-text-shadow center-x-axis center-x-axis -bottom-4 z-10 h-12 w-10/12 border-2 text-xs text-white shadow-2xl transition-all hover:-translate-x-1/2 hover:scale-105 hover:duration-200 active:-translate-x-1/2 active:scale-95 active:opacity-80 active:shadow-none active:duration-75;
@apply retro-text-shadow center-x-axis center-x-axis -bottom-4 z-10 h-12 w-10/12 border-2 text-xs shadow-2xl transition-all hover:-translate-x-1/2 hover:scale-105 hover:duration-200 active:-translate-x-1/2 active:scale-95 active:opacity-80 active:shadow-none active:duration-75;
}
}

54
src/tools/convertTXT.ts Normal file
View File

@ -0,0 +1,54 @@
import { parseData } from './parseData'
interface IBadge {
code: string
name?: string
description?: string
}
const badgePattern = /^badge_(?:name|desc)_([^=]+)/gim
const descriptionPattern = /^badge_desc_(\s*\w+)/
const namePattern = /^badge_name_(\s*\w+)/
export const convertTXT = async (path: string, data: string): Promise<void> => {
const badges: IBadge[] = []
const lines = data.split(/\r?\n/)
for (const line of lines) {
const [key, value] = line.split('=')
const badge = key.match(badgePattern)
if (badge != null) {
if (key.match(namePattern) != null) {
const badgeCode = key.match(namePattern)?.[1] as string
const existingBadge = badges.filter((badge) => {
return badge.code === badgeCode
})[0]
if (Boolean(existingBadge)) {
const index = badges.indexOf(existingBadge)
badges[index].name = value
} else {
badges.push({ code: badgeCode, name: value })
}
} else if (key.match(descriptionPattern) != null) {
const badgeCode = key.match(descriptionPattern)?.[1] as string
const existingBadge = badges.filter((badge) => {
return badge.code === badgeCode
})[0]
if (Boolean(existingBadge)) {
const index = badges.indexOf(existingBadge)
badges[index].description = value
} else {
badges.push({ code: badgeCode, description: value })
}
}
lines.splice(lines.indexOf(line), 1)
}
}
return await parseData(path, 'Badges', badges)
}

View File

@ -0,0 +1,41 @@
import { FigureMap } from '../controllers/FigureMap'
import { EffectMap } from '../controllers/EffectMap'
import { convertTXT } from './convertTXT'
import { FigureData } from '../controllers/FigureData'
import { FurniData } from '../controllers/FurniData'
import { Convertion } from '../config/Convertion'
import { parseData } from './parseData'
export const fetchGamedataConfig = async (data: string, endpoint: GamedataEndpoints): Promise<unknown> => {
switch (endpoint.convert) {
case 'XML':
const convertedData = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '' }).parse(data)
let parsedData: FigureData | FigureMap | EffectMap | undefined
if (endpoint.file_name === 'FigureData') {
parsedData = new FigureData(convertedData, endpoint.file_name)
} else if (endpoint.file_name === 'FigureMap') {
parsedData = new FigureMap(convertedData, endpoint.file_name)
} else if (endpoint.file_name === 'EffectMap') {
parsedData = new EffectMap(convertedData, endpoint.file_name)
}
return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => {
return console.error(error)
})
case 'TXT':
return await convertTXT(Convertion.gamedataDir, data)
default: {
let parsedData: FurniData | undefined
if (endpoint.file_name === 'FurniData') {
parsedData = new FurniData(JSON.parse(data), endpoint.file_name)
}
return await parseData(Convertion.gamedataDir, parsedData?.fileName, parsedData?.data).catch((error) => {
return console.error(error)
})
}
}
}

View File

@ -0,0 +1,47 @@
import { ResponseType } from '@tauri-apps/api/http'
import { GAMEDATA_ENDPOINTS, client } from '../config/Endpoints'
import type { ConvertionHandler } from '../types'
import { parseData } from './parseData'
import { Convertion } from '../config/Convertion'
import { downloadGamedata } from './rusty'
export const handleConvertion = async (
domain: string,
callback: ConvertionHandler,
assetsOption = false
): Promise<void> => {
if (!assetsOption) {
callback('Initializing Gamedata configuration...', 'loading')
const gameData = await GAMEDATA_ENDPOINTS(domain)
await Promise.all(
gameData.map(async (endpoint) => {
if (endpoint.src.startsWith('http')) {
return await client
.get(endpoint.src, { responseType: ResponseType.Text })
.then(async ({ data }) => {
return await downloadGamedata(data as string, endpoint).catch((error) => {
return console.log(error)
})
})
.catch((error) => {
return callback(error, 'error')
})
} else {
return await parseData(Convertion.gamedataDir, endpoint.file_name, endpoint.src)
}
})
)
// callback('Converting shockwave files...', 'loading')
// fetch, read and convert the files from the production folder in the user downloads' folder
// write the files into a seperate folder
} else {
/* ASSETS_ENDPOINTS(domain).map((endpoint) => {
client.get()
}) */
}
}

20
src/tools/parseData.ts Normal file
View File

@ -0,0 +1,20 @@
import { createDir, exists, writeFile } from '@tauri-apps/api/fs'
import { Convertion } from '../config/Convertion'
export const parseData = async (
path: string,
fileName: string | undefined,
fileContent: string | object | undefined
): Promise<void> => {
if (fileName == null || fileContent == null) return
const fileDir = path.concat(`/${fileName}.json`)
// By default, output files will be overwritten. I cannot recursively remove the entire output folder
// and create it again because it just won't parse files' contents for some reason
if (!(await exists(Convertion.gamedataDir))) await createDir(Convertion.gamedataDir, { recursive: true })
return await writeFile(fileDir, typeof fileContent === 'object' ? JSON.stringify(fileContent) : fileContent)
}

17
src/tools/rusty.ts Normal file
View File

@ -0,0 +1,17 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
declare global {
interface Window {
__TAURI_INVOKE__<T>(cmd: string, args?: Record<string, unknown>): Promise<T>;
}
}
const invoke = window.__TAURI_INVOKE__;
export function downloadGamedata(data: string, endpoint: GamedataEndpoints) {
return invoke<null>("download_gamedata", { data,endpoint })
}
export type Converters = "FigureData" | "FigureMap" | "EffectMap" | "FurniData"
export type ConvertTypes = "TXT" | "XML" | "JSON"
export type GamedataEndpoints = { src: string; convert: ConvertTypes; file_name: Converters }

View File

@ -1,29 +1,16 @@
import type {
IEffectMapLibrary,
IFigureDataPalette,
IFigureDataSetType,
IFigureMapLibrary,
IFurni,
IProduct
} from './SubConverters'
import type { IFigureDataPalette, IFigureDataSetType, IFigureMapLibrary, IProduct } from './SubConverters'
export interface IFigureData {
palettes: IFigureDataPalette[]
setTypes: IFigureDataSetType[]
setTypes: Record<string, IFigureDataSetType>
}
export interface IFigureMap {
libraries: IFigureMapLibrary[]
parts: Record<string, Record<number, number>>
}
export interface IFurniData {
roomitemtypes: { furnitype: IFurni }
wallitemtypes: { furnitype: IFurni }
}
export interface IEffectMap {
effects: IEffectMapLibrary[]
}
export type IEffectMap = Record<string, Record<string, string>>
export interface IProductData {
productData: { product: IProduct }

10
src/types/Domain.d.ts vendored
View File

@ -1,10 +0,0 @@
export type DomainTypes =
| 'com.br'
| 'com.tr'
| 'com'
| 'de'
| 'es'
| 'fi'
| 'fr'
| 'it'
| 'nl'

View File

@ -1,3 +0,0 @@
export type GameEndPointsTypes = (
domain: DomainTypes
) => Array<{ src: string; convert?: 'TXT' | 'XML'; fileName: string }>

View File

@ -1,76 +1,40 @@
export interface IFurni {
id: number
classname: string
revision: number
category: string
defaultdir: number
xdim: number
ydim: number
partcolors: { color: string[] }
name: string
description: string
adurl: string
offerid: number
buyout: boolean
rentofferid: number
rentbuyout: boolean
bc: boolean
excludeddynamic: boolean
customparams: string
specialtype: number
canstandon: boolean
cansiton: boolean
canlayon: boolean
furniline: string
environment: string
rare: boolean
}
export type Club = 'idle' | 'HC' | 'VIP'
export interface IFigureDataColor {
id: number
export interface IFigureDataPaletteType {
index: number
club: number // must be changed, either 0, 1, 2
club: number
selectable: boolean
hexCode: string
color: string
}
export interface IFigureDataPalette {
id: number
color: IFigureDataColor[]
}
export type IFigureDataPalette = Record<number, IFigureDataPaletteType>
export interface IFigureDataPart {
id: number
type: string // must be changed
colorable: boolean // must be changed
type: string
colorable: boolean
index: number
colorindex: number
}
export interface IFigureDataHiddenLayer {
partType: string // must be changed
}
export interface IFigureDataSet {
id: number
gender: 'M' | 'F' | 'U'
club: number // 0, 1, 2
colorable: boolean // must be changed
selectable: boolean // must be changed
preselectable: boolean // must be changed
sellable?: boolean // must be changed
club: number
colorable: boolean
selectable: boolean
preselectable: boolean
sellable?: boolean
hiddenLayers?: string[]
parts: IFigureDataPart[]
hiddenLayers?: IFigureDataHiddenLayer[]
}
export interface IFigureDataSetType {
type: string // must be changed
paletteId: number
mandatoryF0: boolean // 0, 1
mandatoryF1: boolean // 0, 1
mandatoryM0: boolean // 0, 1
mandatoryM1: boolean // 0, 1
sets: IFigureDataSet[]
mandatoryF0: boolean
mandatoryF1: boolean
mandatoryM0: boolean
mandatoryM1: boolean
sets: Record<number, IFigureDataSet>
}
export interface IFigureMapLibraryPart {
@ -81,14 +45,6 @@ export interface IFigureMapLibraryPart {
export interface IFigureMapLibrary {
id: string
revision: number
part: IFigureMapLibraryPart[]
}
export interface IEffectMapLibrary {
id: number
lib: string
type: string
revision: number
}
export interface IProduct {

2
src/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
export type StateTypes = 'idle' | 'loading' | 'error' | 'success'
export type ConvertionHandler = (message: string, state: StateTypes) => void

View File

@ -1,4 +1,3 @@
export * from './Converters'
export * from './SubConverters'
export * from './Endpoint'
export * from './Domain'
export * from './global'

View File

@ -1,45 +0,0 @@
import { ResponseType } from '@tauri-apps/api/http'
import { XMLParser } from 'fast-xml-parser'
import { GAME_ENDPOINTS, client } from '../config/ENDPOINTS'
import type { DomainTypes } from '../types/Domain'
import { convertXML } from '../mapping'
import { parseData } from './parseData'
export const fetchGameData = async (
domain: DomainTypes,
callback: (message: string, error: boolean) => void,
assetsOption = false
): Promise<void> => {
if (!assetsOption) {
await Promise.all(
GAME_ENDPOINTS(domain).map(async (endpoint) => {
await client
.get(endpoint.src, { responseType: ResponseType.Text })
.then(async ({ data }) => {
switch (endpoint.convert) {
case 'XML':
const convertedData = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: ''
}).parse(data as string)
const XML2JSON = convertXML(convertedData, endpoint.src)
console.log(XML2JSON)
return await parseData(endpoint.fileName, XML2JSON)
}
})
.catch((error) => {
console.error(error)
return callback(error, true)
})
})
)
} else {
/* ASSETS_ENDPOINTS(domain).map((endpoint) => {
client.get()
}) */
}
}

View File

@ -1,19 +0,0 @@
import { downloadDir } from '@tauri-apps/api/path'
import { createDir, exists, writeTextFile } from '@tauri-apps/api/fs'
import { PROD_VERSION } from '../config/ENDPOINTS'
export const parseData = async (
fileName: string,
fileContent: string | object
): Promise<void> => {
const outputDir = (await downloadDir()).concat(String(PROD_VERSION))
const fileDir = outputDir.concat(`/${fileName}.json`)
if (!(await exists(outputDir))) await createDir(outputDir)
await writeTextFile(
fileDir,
typeof fileContent === 'object' ? JSON.stringify(fileContent) : fileContent
)
}

1
src/vite-env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@ -1,6 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.tsx'],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx,css,md,mdx,html,json,scss}'],
theme: {
extend: {
colors: {

View File

@ -4,8 +4,6 @@
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
@ -14,8 +12,9 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "preserve",
"types": ["vite/client"],
"jsxImportSource": "solid-js"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src"]
}

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

32
vite.config.tauri.ts Normal file
View File

@ -0,0 +1,32 @@
import { defineConfig, mergeConfig } from 'vite'
import baseViteConfig from './vite.config'
import { tauri } from 'vite-plugin-tauri'
// https://vitejs.dev/config/
export default defineConfig(
mergeConfig(
baseViteConfig,
defineConfig({
plugins: [tauri()],
// prevent vite from obscuring rust errors
clearScreen: false,
// Tauri expects a fixed port, fail if that port is not available
server: {
strictPort: true,
open: false
},
// to make use of `TAURI_PLATFORM`, `TAURI_ARCH`, `TAURI_FAMILY`,
// `TAURI_PLATFORM_VERSION`, `TAURI_PLATFORM_TYPE` and `TAURI_DEBUG`
// env variables
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri supports es2021
target: ['es2022', 'chrome100', 'safari13'],
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
}
})
)
);

View File

@ -1,26 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import solidPlugin from 'vite-plugin-solid'
export default defineConfig(async () => ({
plugins: [react()],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ['VITE_', 'TAURI_'],
build: {
// Tauri supports es2021
target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG
}
}))
// https://vitejs.dev/config/
export default defineConfig({
plugins: [solidPlugin()]
})