467 Commits

Author SHA1 Message Date
Misode
39f4ecc504 Fix #824 noise settings preview in 1.18
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2026-02-01 00:16:10 +01:00
Misode
f243be3d5e Fix #832 enchant_with_levels loot preview 2026-02-01 00:07:09 +01:00
Misode
84028a06f1 Add basic support for crafting_dye and crafting_imbue 2026-01-31 23:27:56 +01:00
Misode
ef17f8a5e5 Fix mcdoc version filters for 26.1 2026-01-31 22:14:40 +01:00
OffsetMonkey538
1f8be81f50 Loot Table Modifier generator (#758)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
* Loot Table Modifier loot_modifier generator

Should support everything currently implemented

* Translation keys

* Make 'actions' and 'predicates' always a list

Seems simpler to understand and a list of one entry behaves exactly the same

* Update loot-table-modifier.mcdoc

Should match currently latest commit

* Fix loot modifier path in datapack being incorrect

* Match v2 alpha 1

* Import LootPoolEntry

* Add files via upload

* Revert "Add files via upload"

This reverts commit 7e9c50ee10.

* add condition_add action

* Add union member name overrides

Co-authored-by: Misode <misoloo64@gmail.com>

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2026-01-19 21:06:31 +01:00
Misode
e02c26f6db Fix #839 update spyglass to fix 26.1 version checks
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2026-01-19 20:18:48 +01:00
SpyglassCrafter
8d4e194195 Translated using Weblate (Korean)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 71.7% (277 of 386 strings)

Co-authored-by: lanthanide <lantice3720@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ko/
Translation: Misode's Data Pack Generators/Web App
2026-01-14 06:06:19 +00:00
SpyglassCrafter
1ec1739bfd Translated using Weblate (German)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 98.7% (381 of 386 strings)

Co-authored-by: SoleFrog <solefrog@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/de/
Translation: Misode's Data Pack Generators/Web App
2026-01-12 14:06:19 +00:00
SpyglassCrafter
d1f659f825 Translated using Weblate (German)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 99.2% (383 of 386 strings)

Co-authored-by: Annhilati <annhilati@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/de/
Translation: Misode's Data Pack Generators/Web App
2026-01-08 15:06:19 +00:00
Misode
c8e1714859 Fix article link for new snapshot format
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2026-01-06 20:54:30 +01:00
Misode
1bd7ad880a Fix #835 handle invalid identifiers in mcdoc renderer
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2026-01-03 23:11:48 +01:00
Misode
1836668c1f Fix #833 Update deepslate
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2026-01-03 03:28:39 +01:00
SpyglassCrafter
1b70e60552 Translated using Weblate (Portuguese)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 74.3% (284 of 382 strings)

Co-authored-by: Crimson Developer <crim-dev@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pt/
Translation: Misode's Data Pack Generators/Web App
2025-12-28 17:06:19 +00:00
SpyglassCrafter
8a6232183e Translated using Weblate (German)
Currently translated at 97.9% (374 of 382 strings)

Co-authored-by: [MΛLTE] <malte9799@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/de/
Translation: Misode's Data Pack Generators/Web App
2025-12-28 17:06:19 +00:00
SpyglassCrafter
be7ed77637 Translated using Weblate (French)
Currently translated at 100.0% (382 of 382 strings)

Co-authored-by: Crimson Developer <crim-dev@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/fr/
Translation: Misode's Data Pack Generators/Web App
2025-12-28 17:06:19 +00:00
Misode
a3d8242519 Fix #831 transformation renderer after deepslate update
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-12-21 15:01:55 +01:00
Misode
10b604cf43 Add support for 26.1 snapshots
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-12-17 21:27:26 +01:00
Misode
390844275a Fix #828 account for attribute modifiers with display hidden
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-12-07 01:44:10 +01:00
SpyglassCrafter
c089d70554 Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 98.4% (376 of 382 strings)

Co-authored-by: niangao <niangaovo@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-12-04 09:06:18 +00:00
SpyglassCrafter
971db423f3 Translated using Weblate (Slovak)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 40.3% (154 of 382 strings)

Co-authored-by: Simplmon <simplmon@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/sk/
Translation: Misode's Data Pack Generators/Web App
2025-11-21 09:06:19 +00:00
SpyglassCrafter
a5bcbeebcf Translated using Weblate (French)
Currently translated at 97.9% (374 of 382 strings)

Co-authored-by: M6a5x98 <m6a5x98@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/fr/
Translation: Misode's Data Pack Generators/Web App
2025-11-21 09:06:19 +00:00
SpyglassCrafter
e38ec6725d Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 97.3% (372 of 382 strings)

Co-authored-by: MangoFanFan_ <mangofanfan@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-11-16 14:06:17 +00:00
Misode
3fe6dcdb92 Fix #813 item displays in recipe viewer in 1.20.4
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-11-11 15:23:59 +01:00
SpyglassCrafter
28f6f51d77 Translated using Weblate (French)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 97.8% (370 of 378 strings)

Co-authored-by: ZetMine <zetmine@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/fr/
Translation: Misode's Data Pack Generators/Web App
2025-11-08 19:06:17 +00:00
Misode
558ecbcc5d Add timeline and zombie nautilus variant generators
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-11-05 02:12:49 +01:00
Misode
9a7c325a8c Fix Spyglass breaking change
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-10-20 02:03:30 +02:00
Misode
7e42d13ac1 Update spyglass and add 1.21.11 support 2025-10-20 02:01:08 +02:00
SpyglassCrafter
9c1ed06096 Translated using Weblate (Russian)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 100.0% (378 of 378 strings)

Co-authored-by: Jaga <jagermeistars@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ru/
Translation: Misode's Data Pack Generators/Web App
2025-10-16 23:06:16 +00:00
SpyglassCrafter
e1ec0529a0 Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 97.6% (369 of 378 strings)

Co-authored-by: JerryHan3 <jerryhan3@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-10-14 14:06:16 +00:00
SpyglassCrafter
f57c203de5 Translated using Weblate (Czech)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 96.0% (360 of 375 strings)

Co-authored-by: Simplmon <simplmon@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/cs/
Translation: Misode's Data Pack Generators/Web App
2025-10-09 15:06:16 +00:00
SpyglassCrafter
1ad63894f2 Translated using Weblate (Slovak)
Currently translated at 35.7% (134 of 375 strings)

Co-authored-by: Simplmon <simplmon@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/sk/
Translation: Misode's Data Pack Generators/Web App
2025-10-09 15:06:16 +00:00
Misode
c452033826 Make generator not found errors not reportable
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-10-03 00:16:59 +02:00
ChampionAsh5357
37d90ec493 feat(neoforge): Add new datamaps (#806) 2025-10-02 22:20:09 +02:00
SpyglassCrafter
4158aa85f3 Translated using Weblate (Korean)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 70.1% (263 of 375 strings)

Co-authored-by: JUNG TAEWON <hafskjfha@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ko/
Translation: Misode's Data Pack Generators/Web App
2025-09-18 00:06:15 +00:00
SpyglassCrafter
ec262c576f Translated using Weblate (French)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 99.1% (370 of 373 strings)

Co-authored-by: Thomas <misieur@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/fr/
Translation: Misode's Data Pack Generators/Web App
2025-09-08 15:06:15 +00:00
blockninja124
a33e2b5404 Add ad-astra planets generator (#799)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
* Add ad-astra generator

* Fix file path and min/max version

* Actually fix path 🤦
2025-09-03 23:58:24 +02:00
SpyglassCrafter
9afd315255 Translated using Weblate (German)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 99.1% (370 of 373 strings)

Co-authored-by: Thiemo <travikskoot@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/de/
Translation: Misode's Data Pack Generators/Web App
2025-08-30 17:06:14 +00:00
SpyglassCrafter
688cbd36bf Translated using Weblate (Russian)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 96.7% (361 of 373 strings)

Co-authored-by: GaleevArslanDev <galeevarslandev@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ru/
Translation: Misode's Data Pack Generators/Web App
2025-08-26 16:06:15 +00:00
SpyglassCrafter
818cf736a1 Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 94.1% (351 of 373 strings)

Co-authored-by: JerryHan3 <jerryhan3@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-08-19 07:06:14 +00:00
SpyglassCrafter
00cd1280da Translated using Weblate (French)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 96.7% (358 of 370 strings)

Co-authored-by: cesouRED <cesoured@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/fr/
Translation: Misode's Data Pack Generators/Web App
2025-08-15 21:06:17 +00:00
Daniel Norris
33719ab3e2 Adds Pixelmon's Pokedex datapack types (#791)
* Pixelmon datapack types

* Fix some syntax errors & add NPC Presets

* add pokedex datapack
2025-08-15 17:40:49 +02:00
Misode
d9d2863e3f Fix #777 update spyglass
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-08-08 17:25:05 +02:00
VidTDM
07577f28e8 Update Create Mod Generators to 1.21.1 (#762)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
* Initial Create Recipes

* Minor Changes

* Minor Changes

* fix issue with mixing recipe

* fix issue with cutting recipe

* Update to 1.21.1

* add compacting recipe and fix some issues

* Delete .vscode/snippets.json.code-snippets

* Final Touches

* change warning colour

* Sanitize mcdoc doc comments

* Fix until attributes, as they are exclusive

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2025-08-07 21:53:14 +02:00
SpyglassCrafter
ca36fc9c26 Translated using Weblate (Korean)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 66.2% (245 of 370 strings)

Co-authored-by: Tiji <iamtiji@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ko/
Translation: Misode's Data Pack Generators/Web App
2025-08-07 06:06:14 +00:00
Misode
449e8a2b14 Add version 1.21.9 and set default to 1.21.6 2025-08-07 04:04:21 +02:00
Misode
9e11529b5a Display minor pack formats
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-08-07 03:56:01 +02:00
SpyglassCrafter
c74f17b4f9 Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 94.0% (348 of 370 strings)

Co-authored-by: dongyangchen97-sketch <dongyangchen97-sketch@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-08-03 05:06:16 +00:00
SpyglassCrafter
06bca87f0c Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 90.5% (335 of 370 strings)

Co-authored-by: INF32768 <inf32768@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-07-28 15:06:13 +00:00
SpyglassCrafter
467f36b735 Translated using Weblate (Portuguese)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 72.4% (268 of 370 strings)

Co-authored-by: tmpod <tmpod@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pt/
Translation: Misode's Data Pack Generators/Web App
2025-07-27 00:06:13 +00:00
SpyglassCrafter
c4dc0360ab Translated using Weblate (Japanese)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 91.1% (328 of 360 strings)

Co-authored-by: あれい <azerl0@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ja/
Translation: Misode's Data Pack Generators/Web App
2025-07-16 15:06:13 +00:00
Daniel Norris
ecc810dd4a feat(modded): Adds Pixelmon datapack types (#765)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
* Pixelmon datapack types

* Fix some syntax errors & add NPC Presets
2025-07-14 12:27:53 +02:00
SpyglassCrafter
de9f36a003 Translated using Weblate (Korean)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 63.9% (229 of 358 strings)

Co-authored-by: BinRecycle <binrecycle@users.noreply.weblate.spyglassmc.com>
Co-authored-by: SpyglassCrafter <bot@spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ko/
Translation: Misode's Data Pack Generators/Web App
2025-07-14 02:06:12 +00:00
Nogard
72d927f7a7 Add Czech translations (#768)
* Add Czech translations

* Add Czech locale option to config
2025-07-14 04:05:27 +02:00
Drex
322e4377ed Add fabric:dependency_overrides generator (#770) 2025-07-14 04:03:46 +02:00
Misode
1bc0eb92a2 Fix #775 handle inline dialogs in dialog_list preview 2025-07-14 04:00:28 +02:00
Misode
edf72dd65f Update spyglass + add chicken variant generator 2025-07-14 03:40:09 +02:00
SpyglassCrafter
e8d00bf6cb Translated using Weblate (Japanese)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 91.3% (327 of 358 strings)

Co-authored-by: あれい <azerl0@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ja/
Translation: Misode's Data Pack Generators/Web App
2025-07-08 13:06:13 +00:00
SpyglassCrafter
ac2bb54782 Translated using Weblate (Japanese)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 90.5% (324 of 358 strings)

Co-authored-by: あれい <azerl0@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ja/
Translation: Misode's Data Pack Generators/Web App
2025-07-06 09:06:13 +00:00
SpyglassCrafter
ce6004be7d Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 91.7% (324 of 353 strings)

Co-authored-by: ie9527 <ie9527@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-07-01 18:35:58 +00:00
TheDeathlyCow
71400247bc Thermoo Environment Registries Generators (#754)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
* setup thermoo generator

* constant provider

* seasons provider types

* light threshold

* weather providers

* leaf providers

* precipitation enum and validate relative humidity

* fix for dropdown of env component types

* fix temperature unit enum

* fix seasonal provider

* environment definition mcdoc

* temperature effect set up

* add other temperature effect types

* add custom loot conditions

* add a doc comment to priority

* add temperature effect 1.20 format
2025-06-23 08:53:50 +02:00
SpyglassCrafter
6cae533782 Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 91.7% (324 of 353 strings)

Co-authored-by: 4tubborn <4tubborn@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-06-22 11:06:13 +00:00
SpyglassCrafter
22d13249a5 Translated using Weblate (Korean)
Currently translated at 55.5% (196 of 353 strings)

Co-authored-by: BinRecycle <binrecycle@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ko/
Translation: Misode's Data Pack Generators/Web App
2025-06-22 11:06:13 +00:00
SpyglassCrafter
e38d1349ae Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 87.2% (308 of 353 strings)

Co-authored-by: 4tubborn <4tubborn@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
Translation: Misode's Data Pack Generators/Web App
2025-06-21 10:06:12 +00:00
Misode
e43a938537 Update dialog preview to 1.21.6-pre1
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-06-18 22:17:00 +02:00
Misode
624d47935d Change issue link to mojira.dev
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-06-07 14:21:45 +02:00
SpyglassCrafter
ddf2d8ff0a Translated using Weblate (Portuguese)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 75.6% (267 of 353 strings)

Translated using Weblate (Portuguese)

Currently translated at 75.6% (267 of 353 strings)

Co-authored-by: Miguel <youaren0tme@users.noreply.weblate.spyglassmc.com>
Co-authored-by: O-Zel <o-zel@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pt/
Translation: Misode's Data Pack Generators/Web App
2025-06-02 23:06:13 +00:00
SpyglassCrafter
6e9555dd7f Translated using Weblate (Polish)
Currently translated at 98.8% (349 of 353 strings)

Co-authored-by: Steame <steame090@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
Translation: Misode's Data Pack Generators/Web App
2025-06-02 23:06:13 +00:00
SpyglassCrafter
f19985cb25 Translated using Weblate (French)
Currently translated at 95.7% (338 of 353 strings)

Co-authored-by: Syhix <syhix@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/fr/
Translation: Misode's Data Pack Generators/Web App
2025-06-02 23:06:13 +00:00
SpyglassCrafter
1232ca31d7 Translated using Weblate (Polish)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 98.8% (343 of 347 strings)

Co-authored-by: Steame <steame090@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
Translation: Misode's Data Pack Generators/Web App
2025-06-01 20:06:13 +00:00
SpyglassCrafter
a375525d07 Translated using Weblate (Italian)
Currently translated at 100.0% (347 of 347 strings)

Co-authored-by: Tiziano <t1xx1@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/it/
Translation: Misode's Data Pack Generators/Web App
2025-06-01 20:06:13 +00:00
VidTDM
b1149e3561 Generators for create mod recipe type (#750)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
* Initial Create Recipes

* Minor Changes

* Minor Changes
2025-05-28 20:00:50 +02:00
Misode
4f2c855d1e Add quick output inline toggle
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-05-25 16:14:28 +02:00
Misode
2c5d2434d6 Add atlas and equipment icons
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-05-20 23:07:04 +02:00
Misode
207223dfc6 Hotfix .png.mcmeta implementation
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-05-20 20:52:13 +02:00
Thành Nhân
6684bb0444 Add Vietnamese (vi) language support (#748) 2025-05-20 20:32:00 +02:00
Misode
1c2eccaa71 Add texture .png.mcmeta generator 2025-05-20 20:26:02 +02:00
Misode
ce01e9445a Update dialog preview to 25w21a 2025-05-20 20:09:52 +02:00
Misode
a7141c033e Fix menu results svg stroke color
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-05-19 22:30:35 +02:00
Misode
ae87eb59f0 Add dialog generator icon 2025-05-19 18:29:52 +02:00
Misode
c6777bc32a Fix wrapping long words 2025-05-18 02:04:23 +02:00
Misode
f3d9c7d2eb Add dialog button tooltips 2025-05-18 01:45:27 +02:00
Misode
2655ab9740 Enable wrapping for dialog text components 2025-05-18 00:04:36 +02:00
Misode
c2b11f40cb Use label_format in number_range input control 2025-05-17 23:45:04 +02:00
Misode
511311572d Prevent newlines in dialog buttons 2025-05-17 23:39:34 +02:00
Misode
a5a8c0f261 Fix text and button scale 2025-05-17 23:11:51 +02:00
Misode
0867b629ec Fix #746 text component color resetting to black 2025-05-17 21:19:46 +02:00
Misode
d3f6ef313c Change from hacky text component shadow to css text-shadow 2025-05-17 21:10:14 +02:00
Misode
161b81ffdf Fix #745 item dialog body scaling 2025-05-17 20:43:07 +02:00
Misode
c733c3be75 Fix minor dialog issues 2025-05-17 20:08:40 +02:00
Misode
e98861aea1 Fix "Show more" versions 2025-05-17 18:38:54 +02:00
Misode
5c77276de1 Add dialog preview 2025-05-17 18:29:07 +02:00
Misode
70c82984f8 Remember preview panel open state + don't hide source panel 2025-05-17 13:05:31 +02:00
Misode
abfb8959ca Add dialog tag generator 2025-05-17 13:02:28 +02:00
Misode
21c0d65425 Add dialog generator 2025-05-14 01:22:37 +02:00
SpyglassCrafter
8fdb3873bc Translated using Weblate (German)
Currently translated at 100.0% (347 of 347 strings)

Co-authored-by: Paulantis <paulantis@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/de/
Translation: Misode's Data Pack Generators/Web App
2025-05-11 10:06:14 +00:00
SpyglassCrafter
6654f39d74 Translated using Weblate (Portuguese)
Currently translated at 75.7% (263 of 347 strings)

Co-authored-by: O-Zel <o-zel@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pt/
Translation: Misode's Data Pack Generators/Web App
2025-05-03 15:06:13 +00:00
SpyglassCrafter
f0b03b6785 Translated using Weblate (Polish)
Currently translated at 98.8% (343 of 347 strings)

Co-authored-by: Bartus131313 <bartus131313@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
Translation: Misode's Data Pack Generators/Web App
2025-05-03 15:06:13 +00:00
SpyglassCrafter
f518177267 Translated using Weblate (Russian)
Currently translated at 100.0% (347 of 347 strings)

Co-authored-by: TrRuki <TrRuki.mail@gmail.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ru/
Translation: Misode's Data Pack Generators/Web App
2025-04-29 23:06:13 +00:00
SpyglassCrafter
6a46399e86 Translated using Weblate (Polish)
Currently translated at 98.8% (343 of 347 strings)

Co-authored-by: Technicman69 <technicman69@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
Translation: Misode's Data Pack Generators/Web App
2025-04-27 13:06:13 +00:00
SpyglassCrafter
aba3acb452 Translated using Weblate (Spanish)
Currently translated at 65.7% (228 of 347 strings)

Co-authored-by: IsuKun200 <isukun200@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/es/
Translation: Misode's Data Pack Generators/Web App
2025-04-24 21:06:14 +00:00
SpyglassCrafter
8d0317e2f1 Translated using Weblate (German)
Currently translated at 42.0% (146 of 347 strings)

Co-authored-by: TRC Loop // AK <trc-loop@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/de/
Translation: Misode's Data Pack Generators/Web App
2025-04-24 21:06:14 +00:00
Misode
05d061de77 Fix 1.21.5 text component handling in loot table preview 2025-04-21 18:50:06 +02:00
Misode
df0c9639c5 Update dependencies
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-04-12 19:03:30 +02:00
Misode
74767306cd Fix #683 and fix #698 update deepslate 2025-04-12 19:01:43 +02:00
Misode
108bc15cb2 Add 1.21.6 and set default to 1.21.5
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-04-12 17:15:13 +02:00
Misode
474b2dfe52 Fix #718 incomplete table_bonus condition 2025-04-12 16:52:52 +02:00
Misode
844cc44ac7 Merge branch 'master' of https://github.com/misode/misode.github.io 2025-04-12 16:43:14 +02:00
Misode
123ef47660 Fix #734 move controls popup above source panel 2025-04-12 16:43:10 +02:00
SpyglassCrafter
d021cf0a51 Translated using Weblate (Italian)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Currently translated at 38.6% (134 of 347 strings)

Co-authored-by: Leo <mtcleo05@users.noreply.weblate.spyglassmc.com>
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/it/
Translation: Misode's Data Pack Generators/Web App
2025-04-11 08:06:12 +00:00
Misode
8d610a358b Fix Text mcdoc references
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-04-07 01:20:45 +02:00
Misode
31b4c66f40 Fix 1.21.5 version ref 2025-04-07 01:17:37 +02:00
Karol319
11c375d01b Translated using Weblate (Polish)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Currently translated at 95.9% (333 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-05 21:06:12 +00:00
Lepek8777
ecda30e842 Translated using Weblate (Polish)
Currently translated at 95.9% (333 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-05 21:06:12 +00:00
Lepek8777
2437d23037 Translated using Weblate (Polish)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Currently translated at 90.2% (313 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 21:02:38 +00:00
Lepek8777
bd1e6a10ec Translated using Weblate (Polish)
Currently translated at 89.0% (309 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 21:02:09 +00:00
Karol319
f2a1a1db95 Translated using Weblate (Polish)
Currently translated at 89.0% (309 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 21:02:09 +00:00
Karol319
c1a934d627 Translated using Weblate (Polish)
Currently translated at 88.1% (306 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 21:01:15 +00:00
Lepek8777
aaee259fbf Translated using Weblate (Polish)
Currently translated at 88.1% (306 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 21:01:15 +00:00
Karol319
02ce8febfd Translated using Weblate (Polish)
Currently translated at 85.8% (298 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:59:45 +00:00
Lepek8777
0825495399 Translated using Weblate (Polish)
Currently translated at 85.8% (298 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:59:45 +00:00
Karol319
2daf024649 Translated using Weblate (Polish)
Currently translated at 82.4% (286 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:57:04 +00:00
Lepek8777
ed75a8a2a0 Translated using Weblate (Polish)
Currently translated at 82.4% (286 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:57:04 +00:00
Karol319
a52ef94b97 Translated using Weblate (Polish)
Currently translated at 80.4% (279 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:53:35 +00:00
Lepek8777
1fb264b688 Translated using Weblate (Polish)
Currently translated at 80.4% (279 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:53:35 +00:00
Karol319
b2a7409536 Translated using Weblate (Polish)
Currently translated at 72.3% (251 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:47:45 +00:00
Lepek8777
bfd9ead414 Translated using Weblate (Polish)
Currently translated at 72.3% (251 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:47:45 +00:00
Karol319
df97c84a50 Translated using Weblate (Polish)
Currently translated at 68.5% (238 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:44:21 +00:00
Lepek8777
df16b85af4 Translated using Weblate (Polish)
Currently translated at 68.5% (238 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:44:21 +00:00
Lepek8777
3dcf4382e4 Translated using Weblate (Polish)
Currently translated at 66.2% (230 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:42:31 +00:00
Karol319
cebe9a2e16 Translated using Weblate (Polish)
Currently translated at 66.2% (230 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:42:31 +00:00
Karol319
05ee8098d9 Translated using Weblate (Polish)
Currently translated at 62.8% (218 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:40:03 +00:00
Lepek8777
002fa9f2ec Translated using Weblate (Polish)
Currently translated at 62.8% (218 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/pl/
2025-04-04 20:40:03 +00:00
𝑯𝒆𝒂𝒍𝒆𝒓𝑷𝒍𝒖𝒔𝑴𝑪
8ec60e1ca2 Update ar.json (#717)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
* Update ar.json

* Update ar.json
2025-03-17 14:34:24 +01:00
DerIch69420
6e7ca4a1c5 Translated using Weblate (German)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 26.8% (93 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/de/
2025-03-14 14:06:14 +00:00
Misode
0138b54e0b Only show the top supporters on hompage
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
I no longer have time to maintain the full contributors list unfortunately
2025-03-11 21:47:51 +01:00
Misode
b8c65edd83 Update vanilla-mcdoc references
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-03-07 01:00:37 +01:00
Misode
f91c1be41c Revert "Use Spyglass API to get vanilla-mcdoc symbols"
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
This reverts commit 63f9eed07c.
2025-03-04 18:46:14 +01:00
𝑯𝒆𝒂𝒍𝒆𝒓𝑷𝒍𝒖𝒔𝑴𝑪
cb15e6ccd1 Translated using Weblate (Arabic)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 43.2% (150 of 347 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ar/
2025-03-02 20:06:10 +00:00
JerryHan3
4d314ee02e Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 84.6% (293 of 346 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
2025-02-28 09:06:12 +00:00
Misode
63f9eed07c Use Spyglass API to get vanilla-mcdoc symbols
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-02-27 22:14:44 +01:00
Misode
331a4e2fe2 Format
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-02-22 19:34:39 +01:00
Sol Toder
3f52a908a6 Fix converting colors with alpha to hex RGB (#705)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Convert negative integers to unsigned integers. Truncate the alpha from colors before passing them to the HTML color picker, since it doesn't allow alpha values (and they'd be in the wrong place if it did).
2025-02-22 15:14:57 +01:00
Misode
6bf9fcb76e Add wolf sound variant to en.json
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-02-20 00:12:58 +01:00
Misode
b23be41bee Add support for wolf_sound_variant folder 2025-02-20 00:06:39 +01:00
Misode
4d830d9d61 Add Arabic language
Co-authored-by: HealerPlusMC <healerplus908@gmail.com>
2025-02-19 22:49:54 +01:00
Misode
5d2306792c Added translation using Weblate (Arabic) 2025-02-19 21:36:43 +00:00
Jaga
bb16baec58 Translated using Weblate (Russian)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Currently translated at 100.0% (346 of 346 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ru/
2025-02-19 15:06:12 +00:00
Ethan Costa
b7672e42c3 Change sky aesthetics folder (#701)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-02-17 23:20:47 +01:00
Misode
d80a6827e3 Fix #696 loot table preview error
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-02-12 21:01:13 +01:00
fuuuuuuuuuuuuuuck
deca0f1fe2 Translated using Weblate (Chinese (Simplified Han script))
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Currently translated at 83.8% (290 of 346 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
2025-02-11 16:05:56 +00:00
Night-Aurora
b31ad73a26 Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 71.9% (249 of 346 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
2025-02-05 18:06:09 +00:00
LinRan
db1bf36b1f Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 70.2% (243 of 346 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
2025-02-02 06:06:11 +00:00
Misode
0dee553d7e Add generator aliases for search
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-01-30 00:10:06 +01:00
Misode
c44678818d Merge branch 'master' of https://github.com/misode/misode.github.io 2025-01-29 23:40:38 +01:00
Misode
855f9adc32 Update vite 2025-01-29 23:40:35 +01:00
DevMetal00
98ab037f82 Translated using Weblate (Spanish)
Currently translated at 59.8% (203 of 339 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/es/
2025-01-29 21:06:12 +00:00
Misode
507e37babf Add cow_variant and update spyglass 2025-01-29 21:13:26 +01:00
Misode
59642b2ff5 Use fancy menu for presets
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-01-28 01:14:35 +01:00
Misode
111855f3ea Make generator switcher a generic component 2025-01-28 00:40:28 +01:00
Misode
c4a9bc06fa Add keyboard navigation to generator switcher
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-01-27 23:13:10 +01:00
Misode
953425b800 Add documenation for modded generators
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-01-24 18:39:10 +01:00
Misode
20498e84c1 Add assets badge
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-01-24 02:14:32 +01:00
Misode
00fe95a400 Add new generator switcher menu 2025-01-24 01:56:16 +01:00
Misode
46b066e1b6 Update github actions 2025-01-24 00:40:50 +01:00
Misode
3a9f836035 Include stack trace in file view error window
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-01-23 21:42:48 +01:00
Misode
8c67166733 Link to technical changes repo
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-01-23 01:15:32 +01:00
Misode
c1fc6fc2f1 Support 24w04a 2025-01-23 00:51:42 +01:00
Misode
96278cbbe4 Merge branch 'master' of https://github.com/misode/misode.github.io
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-01-16 01:10:57 +01:00
Misode
f656ab2ee8 25w03a 2025-01-16 01:10:54 +01:00
Jaga
f4b91fc817 Translated using Weblate (Russian)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 100.0% (339 of 339 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ru/
2025-01-14 16:06:08 +00:00
Jaga
e3c22841a6 Translated using Weblate (Russian)
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 100.0% (338 of 338 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ru/
2025-01-11 19:06:11 +00:00
Syhix
3445a3ee59 Translated using Weblate (French)
Currently translated at 100.0% (338 of 338 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/fr/
2025-01-11 19:06:11 +00:00
ChampionAsh5357
9ad376e391 fix(neoforge): Remove carvestep from 1.21.2 onwards (#676) 2025-01-11 19:27:17 +01:00
Misode
a67afdbe6a Add 1.21.5 and pig_variant, set default version to 1.21.4
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-01-09 01:16:00 +01:00
Anonymous
9697b3e34e Translated using Weblate (Chinese (Simplified Han script))
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
Currently translated at 71.0% (240 of 338 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hans/
2025-01-05 18:06:11 +00:00
Anonymous
858f9a42c6 Translated using Weblate (Korean)
Currently translated at 56.8% (192 of 338 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/ko/
2025-01-05 18:06:11 +00:00
Misode
a97c21d9de Re-add custom page_view event
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-01-04 20:42:04 +01:00
Misode
e52e6a791c Remove lagacy pageview event 2025-01-04 20:27:29 +01:00
Misode
2eab79a8ed Add section with info about forking to the readme 2025-01-04 19:05:54 +01:00
Misode
55dc606ddc Have a single place to define the repo url 2025-01-04 19:05:23 +01:00
Misode
0b73a6056a Remove legacy GA4 code 2025-01-04 19:05:08 +01:00
Misode
66c6c814ef Update localization links in readme 2025-01-04 18:30:07 +01:00
Misode
2fd90f5dd4 Add item model preview
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2025-01-03 05:17:19 +01:00
Misode
4fd668e44c Fix preview panel not showing for model generator
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2025-01-03 04:49:12 +01:00
Misode
1514ec6a32 Fix #668 remove duplicate enchantment tag generator
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-12-28 14:28:08 +01:00
Misode
d29d612edb Fix dependency label on modded generator cards
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-12-28 00:46:39 +01:00
Ethan Costa
2bca1f1ffb Add Custom Sky generator for Sky Aesthetics (#667)
* Added Custom Sky generator

* Added 1.21.1 support

* fix config

* revert config change

* Fix assets config not working

* update url

* Fix merge conflict

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-12-27 22:42:50 +01:00
Misode
09a2d4c24f Fix fabric.mod.json entrypoints
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-12-27 05:09:18 +01:00
Misode
d41ac45403 Add fabric.mod.json generator 2024-12-27 04:44:35 +01:00
Misode
a52dbaa758 Fix density function numeric inaccuracies
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-12-23 23:05:03 +01:00
Misode
87522bc1ec Update spyglass versions and remove mcdoc patch
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-12-13 22:11:20 +01:00
Misode
da910f42b4 Fix #649 optimize page load by using mcdoc symbols export 2024-12-13 22:00:00 +01:00
Misode
cc5b79b4e5 Fix #654 disable the remaining simplifiedTypeDef caching logic in mcdoc
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-12-10 22:46:05 +01:00
Misode
1860f86cb0 Fix #641 format JSON output
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-12-10 18:03:00 +01:00
Misode
7ceb74fa15 Fix #652 add pack.mcmeta when downloading project zip 2024-12-10 16:17:42 +01:00
Misode
22d35ef6a8 Make hide_tooltip actually hide the tooltip
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-12-06 19:23:38 +01:00
Misode
c066d88518 Merge branch 'master' of https://github.com/misode/misode.github.io 2024-12-06 19:20:55 +01:00
Misode
e844477c80 Fix #626 update item displays to 1.21.4 2024-12-06 19:20:48 +01:00
Alexandre Collignon
fb7012b9f4 feat: Added missing key langs for French (#653)
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-12-05 21:49:51 +01:00
Misode
c81bafd674 Fix preview scaling
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-12-03 21:24:46 +01:00
Misode
a8aaec69e2 Handle invalid set_lore functions 2024-12-03 21:14:26 +01:00
Misode
1d1bae9459 Add error boundary for preview panel 2024-12-03 21:13:33 +01:00
Misode
8f37b45ae1 Fix copy and download triggers re-activating when document changes
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-11-29 18:20:33 +01:00
Misode
fabdb1aed3 Add recipe output convert format 2024-11-29 16:34:20 +01:00
Misode
f694b56244 Update versions in index.html title
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-11-29 07:04:01 +01:00
Misode
061c1d5cef Static render all combinations of convert formats 2024-11-29 07:03:19 +01:00
Misode
7501b392be Add item modifier as convert format 2024-11-29 06:37:39 +01:00
Misode
a948715523 Add /give to loot table converter tool 2024-11-29 06:08:01 +01:00
Misode
fb95b386cc Sort string completions alphabetically
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-11-29 01:54:12 +01:00
Misode
114ba13be9 Fix #644 pack.mcmeta saving
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-11-28 17:28:44 +01:00
Misode
94a8210e4c Fix #642 more validation in loot table preview
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-11-27 23:40:07 +01:00
Misode
dc72614e85 Fix #637 unsupported project storage undefined on first load 2024-11-27 22:39:49 +01:00
Misode
5c874e3f8a Fix #618 typo in customized ore editor 2024-11-27 21:51:00 +01:00
Misode
882178c208 Fix #630 base attack damage amount 2024-11-27 21:46:47 +01:00
Misode
794657d0f5 Fix #640 handle malformed set_enchantments in loot table preview 2024-11-27 21:40:39 +01:00
Misode
fdcb3f2b70 Fix #638 move minVersion from model to equipment 2024-11-27 21:21:52 +01:00
Misode
8288397bc9 Upgrade draft project to hardcoded drafts uri 2024-11-27 21:09:12 +01:00
Misode
b1c8bba04a Add color indicator to enum fields
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
2024-11-27 19:15:22 +01:00
Misode
95f4363840 Improve input fields in mcdoc renderer 2024-11-27 18:59:25 +01:00
Misode
c3d0f133ab Enable vanilla mcdoc refresh 2024-11-27 18:07:08 +01:00
Misode
f18b4baf73 Add equipment generator 2024-11-27 17:37:36 +01:00
Misode
11484df930 Two hot fixes 2024-11-27 17:06:37 +01:00
Misode
aef57e7f00 Merge pull request #614 from misode/mcdoc 2024-11-27 16:57:33 +01:00
Misode
1abc28f369 Get project data for worldgen previews 2024-11-27 06:01:05 +01:00
Misode
2bc0fc23d8 Disable mcdoc simplified type caching 2024-11-27 04:42:51 +01:00
Misode
a86a707232 Exclude already existing keys from dynamic key suggestions 2024-11-27 01:28:29 +01:00
Misode
145bced7d2 Temporarily keep the mcdoc dependencies 2024-11-27 01:03:43 +01:00
Misode
8415340557 Insert new fields in original struct order 2024-11-27 00:29:21 +01:00
Misode
283248911b Fix issue when switching versions and generators at the same time 2024-11-27 00:11:43 +01:00
Misode
2f5f18777d Fix project not opening after creation 2024-11-26 19:25:20 +01:00
Misode
e9280a7a54 Remove unused memory and mixed file systems for now 2024-11-26 18:05:17 +01:00
Misode
6badc9f06f Fix indexeddb file system readdir to use a range 2024-11-26 18:04:11 +01:00
Misode
46ed105c34 Optimize spyglass external hash function 2024-11-26 02:22:34 +01:00
Misode
3a467e54b7 Improve doc comment rendering in markdown 2024-11-26 01:15:36 +01:00
Misode
d370b4244a Skip encoding/decoding step when importing and downloading projects 2024-11-26 01:15:24 +01:00
Misode
ef03fe6058 Allow newlines in strings by special casing "\n" 2024-11-25 18:07:20 +01:00
Misode
ac37928557 Add lang and post_effect generators 2024-11-25 18:02:37 +01:00
Misode
b54abd5273 Update spyglass to better support resource packs 2024-11-25 17:42:20 +01:00
Misode
a2d8adbc4b Keep preset and share search params in the url 2024-11-21 05:41:15 +01:00
Misode
4bbff0969f Clear dependencies correctly 2024-11-21 05:16:59 +01:00
Misode
b535f55e76 Upgrade legacy project files from localstorage to indexeddb 2024-11-21 05:09:39 +01:00
Misode
fa1f852822 Fix project panel flicker and filter out directories 2024-11-21 02:05:51 +01:00
Misode
7719112b83 Register JE uri binder 2024-11-21 01:45:30 +01:00
Misode
5cfd17a107 Fix root hashing due to missing root dirs 2024-11-21 01:30:42 +01:00
Misode
4bc6e758da Handle doc load errors 2024-11-21 00:48:02 +01:00
Misode
f3de707224 Add skeleton loading indicators 2024-11-20 22:06:29 +01:00
Misode
56b2e1a382 Make project panel resizable 2024-11-20 20:28:54 +01:00
Misode
22e787bf4e Delete files when deleting a project 2024-11-20 04:37:11 +01:00
Misode
e11e88d6db Render unknown fields 2024-11-20 03:42:37 +01:00
Misode
f4f2450133 Refactor passing makeEdit around in ctx 2024-11-19 20:57:18 +01:00
Misode
11029e88f6 Collapse surface rules by default 2024-11-19 19:38:49 +01:00
Misode
bf9590a225 Tweak collapse styles 2024-11-19 18:15:35 +01:00
Misode
8b58b2c7a4 Improve dynamic struct fields 2024-11-19 16:36:35 +01:00
Misode
0b7e9b6948 Remove unused data-cy attributes 2024-11-19 15:14:10 +01:00
Misode
72fe13fcdc Merge branch 'master' of https://github.com/misode/misode.github.io into mcdoc 2024-11-19 05:01:34 +01:00
Misode
831d4e7706 Fix Ctrl+S for saving a file 2024-11-19 04:51:24 +01:00
Misode
06b028d04f Fix for switching back to latest version 2024-11-19 04:43:53 +01:00
Misode
988e24f337 Update project panel tree view when files change 2024-11-19 04:28:04 +01:00
Misode
b0963b1163 Implement renaming files 2024-11-19 03:26:00 +01:00
Misode
14abe1ee52 Refactor modals to use context provider 2024-11-18 23:55:14 +01:00
Misode
5db012f101 Simplify spyglass imports 2024-11-18 17:16:38 +01:00
Misode
2366716cae Refactor projects to use indexeddb 2024-11-13 05:29:06 +01:00
Misode
55f961c8cc Fix item tint color list length
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-11-09 21:15:23 +01:00
Misode
ad5c45087e Fix item schema
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-11-08 18:52:17 +01:00
Misode
09cbf7b59a Add assets/items generator and support 24w45a
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-11-06 21:01:24 +01:00
Misode
26079d1188 Sort overlay file systems based on prefix length 2024-11-06 06:53:05 +01:00
Misode
c726689459 Implement memory file system and mixed file system 2024-11-06 06:31:59 +01:00
Misode
760f256aa1 Split FileSystem 2024-11-06 01:24:28 +01:00
Misode
adc355d347 Disable item display for item tag fields 2024-11-04 22:43:59 +01:00
Misode
807f236f2a Improve switching between union members 2024-11-04 21:14:20 +01:00
Misode
c80c500386 Add support for mcdoc:block_state_keys 2024-11-02 07:43:30 +01:00
Misode
926d19bb56 Add block state validation 2024-11-02 06:35:06 +01:00
Misode
4d85a9f491 Implement set generator default 2024-11-01 18:33:25 +01:00
Misode
7b576da9d2 Clean up file opening and watcher events 2024-11-01 17:45:54 +01:00
Misode
a380999afb Implement IndexedDB file system 2024-11-01 05:08:23 +01:00
Misode
00f0c09a34 Add neoforge and immersiveweathering mcdoc 2024-11-01 00:19:46 +01:00
Misode
68eb077c17 Re-add ohthetreesyoullgrow partner 2024-10-31 06:41:30 +01:00
Misode
cf44c3236f Add duplicate action and split ListItem component 2024-10-31 05:02:57 +01:00
Misode
4cf1b16a86 Show max 50 list items by default 2024-10-31 04:20:02 +01:00
Misode
0214a6ea7b Make list and dynamic struct children collapsible 2024-10-31 01:37:34 +01:00
Misode
88c8719eab Render short numeric tuples inline 2024-10-31 01:07:01 +01:00
Misode
ea873cae22 Move stuff around to group mcdoc types 2024-10-31 00:09:37 +01:00
Misode
444173cd78 Split mcdoc file into components and helpers 2024-10-31 00:09:33 +01:00
Misode
a76347d2b5 Merge branch 'master' of https://github.com/misode/misode.github.io into mcdoc 2024-10-30 17:37:36 +01:00
Misode
9886155fee Manually filter out some fields due to probable spyglass bug 2024-10-30 17:32:20 +01:00
Misode
377c3f1e65 Add 1.21.4
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-10-30 16:54:38 +01:00
Misode
083862f867 Improve key formatting and use enum identifiers 2024-10-29 06:13:12 +01:00
Misode
a9e7e88f9c Improve contrast of function category colors 2024-10-29 05:54:40 +01:00
Misode
588b7a578e Set root category for item modifiers and predicates 2024-10-29 05:46:26 +01:00
Misode
d6118dcb99 Tweak node button colors 2024-10-29 05:21:43 +01:00
Misode
3f0855d336 Fix optional literals 2024-10-29 04:42:07 +01:00
Misode
de084e030c Escape quotes when editing strings 2024-10-29 04:21:19 +01:00
Misode
65a394c3ed Improve union member matching 2024-10-29 03:31:16 +01:00
Misode
7770b97a7a Implement any/unsafe type 2024-10-29 02:41:59 +01:00
Misode
a536c78e05 Fix error with empty union 2024-10-28 06:18:32 +01:00
Misode
bdc06d1f43 Fix dispatcher types not always having context 2024-10-28 06:03:24 +01:00
Misode
9fa9da0230 Fix assets generators 2024-10-27 06:03:58 +01:00
Misode
446e04879c Add random seed button 2024-10-27 05:39:04 +01:00
Misode
88b8730b72 Fix world settings generator 2024-10-27 05:15:26 +01:00
Misode
3447a586c2 Fix text component generator 2024-10-27 04:59:50 +01:00
Misode
4d9e9fa40c Remove unnecessary json parsing for presets and snippets 2024-10-26 23:01:49 +02:00
Misode
e130cb59eb Merge branch 'master' of https://github.com/misode/misode.github.io into mcdoc 2024-10-26 22:47:52 +02:00
Misode
e8871ee02a Also show item display for block registry 2024-10-26 22:46:15 +02:00
Misode
2163020db8 Pass runtimeKey when simplifying dynamic fields 2024-10-26 22:41:09 +02:00
Misode
8d3497e7b6 Format data components 2024-10-26 22:04:48 +02:00
Misode
2ff59b8405 Wrap all JSON.parse calls with try-catch 2024-10-26 21:49:12 +02:00
Misode
6555e80ead Hide [any]: any dynamic fields 2024-10-26 15:57:33 +02:00
Misode
b9e72bc4da Don't rely on the file root typeDef + refactor mcdoc props 2024-10-25 20:00:58 +02:00
Misode
35de8bc538 Implement dynamic struct fields + improve UX 2024-10-25 06:18:29 +02:00
Misode
8bc821e516 Make fixed lists and tuples consistent 2024-10-25 02:44:29 +02:00
Misode
1f0a3a03a9 Change style of objects inside lists 2024-10-25 01:23:36 +02:00
Misode
18dd627ad8 Fix issue with setting strings to unset 2024-10-24 23:32:56 +02:00
Misode
974730ff44 Implement add to bottom of list 2024-10-24 23:23:07 +02:00
Misode
ac43582627 Use simplified type when getting default value 2024-10-24 23:09:13 +02:00
Misode
a79fd5fd6c Add item display and follow reference button 2024-10-24 22:47:26 +02:00
Misode
c25a066161 Implement move up and move down buttons 2024-10-24 22:33:26 +02:00
Misode
3e72588dc9 Validate regex patterns, fix object bodies 2024-10-24 21:41:46 +02:00
Misode
6649b0aabd Change pack format validation again 2024-10-24 21:28:08 +02:00
Misode
874b9cdc33 Improve unset unions 2024-10-24 21:01:47 +02:00
Misode
e71267f1de Keep required empty strings 2024-10-24 19:08:16 +02:00
Misode
f49202c160 Handle "missing key" errors separately 2024-10-24 18:39:13 +02:00
Misode
a3faa4a3c9 Format select registries, enums and unions 2024-10-24 17:54:39 +02:00
Misode
599b7ea068 Make 1.21.2 the default version
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled
2024-10-24 16:26:07 +02:00
Misode
ee655b39e5 Remove unnecessary json parse and stringify step when importing 2024-10-24 15:57:14 +02:00
Misode
9f1ae01d91 Rename variables to be more consistent 2024-10-24 15:24:04 +02:00
Misode
3f7c8f6904 Add hardcoded colored categories 2024-10-24 06:50:04 +02:00
Misode
c9f216c550 Change doc comments to be based on field instead of hover 2024-10-24 06:15:04 +02:00
Misode
b66b53ceaa Show doc comments 2024-10-24 06:07:12 +02:00
Misode
256390cbd2 Tweak pack format error reporting 2024-10-24 05:40:04 +02:00
Misode
9066469381 Add error indicators 2024-10-24 05:02:37 +02:00
Misode
33d4c30539 Add color pickers 2024-10-24 03:59:04 +02:00
Misode
e9f16aa3f7 Add literal and tuple types + improve struct and list default 2024-10-24 03:31:34 +02:00
Misode
039910e43e Handle unions matching multiple members 2024-10-24 02:29:38 +02:00
Misode
22409f62ce Add string datalist completions 2024-10-24 02:09:17 +02:00
Misode
d9a1d4c41a Implement enums and unions 2024-10-24 01:21:53 +02:00
Misode
6c214d4e3a Simplify mcdoc fields when rendering 2024-10-24 00:34:32 +02:00
Misode
6e68de01aa Refactor makeEdit so it returns the result node 2024-10-23 21:41:12 +02:00
Misode
6151fbcea4 List add button 2024-10-23 20:14:10 +02:00
Misode
6ea2b7929c Add string, numeric and boolean editing 2024-10-23 18:46:21 +02:00
Misode
18332b9dbc Make edits to AST and then use formatter 2024-10-23 06:10:14 +02:00
Misode
a0f3e71000 Refactor spyglass service and context 2024-10-23 05:44:20 +02:00
Misode
c358c871da Implement undo and redo 2024-10-22 22:55:04 +02:00
Misode
ea37eb168f Keep track of opened documents and prepare for undo/redo 2024-10-22 15:43:02 +02:00
Misode
9cb7f7297c Make Spyglass a singleton object 2024-10-20 20:23:17 +02:00
Misode
d248732469 Basic mcdoc tree rendering 2024-10-17 15:14:30 +02:00
Misode
7ed34a61e7 Add spyglass context and file watcher 2024-10-16 15:46:17 +02:00
Misode
77d6323219 Use spyglass DocAndNode to store current file data 2024-10-16 04:36:59 +02:00
Misode
7dbd533abb Add mcmeta-summary symbol registrar and initialize remaining 2024-10-15 23:25:56 +02:00
Misode
60aab0c6b9 Initialize spyglass project and load vanilla-mcdoc 2024-10-15 07:24:12 +02:00
Misode
ccdcf9e7e3 🔥 Nuke all mcschema related code 2024-10-15 05:14:02 +02:00
Misode
b9a23d0f47 Fix #613 issues with inline instrument component 2024-10-15 03:38:12 +02:00
Misode
505842a319 Adjust quote-props eslint rule 2024-10-12 01:12:51 +02:00
Misode
04864cea2d Update version article links 2024-10-11 06:06:25 +02:00
Misode
a8abe6355f Update supporters 2024-10-08 01:34:11 +02:00
Misode
763b70180f Fix git diff including 404 responses 2024-10-04 19:50:14 +02:00
Misode
5b5cc026f4 Add snbt and shaders to text-like files 2024-10-04 19:23:21 +02:00
Misode
91f61b3c36 Fix #519 by manually creating the patch when github doesn't give it 2024-10-04 19:02:53 +02:00
Misode
7757dbcac3 Fix #547 quote snbt keys + respect indentation 2024-10-04 18:12:01 +02:00
Misode
23ab957f62 Fix tree not refreshing when importing on mobile 2024-10-04 17:42:52 +02:00
Misode
f1b60b8b40 Add paste button on mobile + update copy icon (#610) 2024-10-04 17:40:02 +02:00
Misode
fab3088799 Close #537 implement alphabetical sort in output 2024-10-04 16:42:47 +02:00
Misode
22c8566819 Fix #463 root list nodes didn't have "add to bottom" button due to hook weirdness 2024-10-04 16:08:39 +02:00
Misode
fec84a03d2 Fix #540 by validating file names 2024-10-04 15:52:21 +02:00
Misode
5d08f15006 Fix #319 keep unknown files when importing and downloading a project 2024-10-04 15:30:35 +02:00
ChampionAsh5357
394beeab16 Add NeoForge Data Generators (#608)
* feat(partner): Add neoforge integrations

* Fix build and format file

* fix(partner): Handle matching within choice node

* Tweak some of the ChoiceNode's

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-10-04 05:10:33 +02:00
Misode
480c8b5f35 Swap popular generator predicate -> recipe 2024-10-03 05:04:22 +02:00
Misode
f893c1e2d4 Hide pack format bumps on the homepage 2024-10-03 04:56:04 +02:00
Misode
2344753db3 Update deepslate to fix bell and sign item rendering 2024-10-03 01:43:18 +02:00
Misode
7754d361c3 Fix #301 update deepslate with new special renderers 2024-10-02 17:17:56 +02:00
Misode
75c662863c Fix #459 Store DF mode to local storage 2024-09-30 19:38:24 +02:00
Misode
c8be49d8bc Update browserlist-db 2024-09-30 18:45:14 +02:00
Misode
3b0a733eaf Fix #603 switching to "multiple" in Tag predicate schema 2024-09-30 18:45:14 +02:00
dependabot[bot]
8d38f0553a Bump rollup from 2.79.1 to 2.79.2 (#607)
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.1 to 2.79.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.1...v2.79.2)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 20:22:56 +02:00
Misode
88b7b74ca0 Fix #599 catch invalid lore lines 2024-09-18 17:38:31 +02:00
Misode
586c777cf8 Support 24w37a and 24w38a 2024-09-18 17:03:57 +02:00
dependabot[bot]
52747deaea Bump vite from 3.2.10 to 3.2.11 (#606)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.10 to 3.2.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.11/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-18 14:59:21 +02:00
dependabot[bot]
6750fbaf75 Bump vite from 3.2.7 to 3.2.10 (#499)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 3.2.7 to 3.2.10.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v3.2.10/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v3.2.10/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-18 14:56:28 +02:00
Misode
137c4816d6 Allow generators to be listed in two columns 2024-09-18 14:45:50 +02:00
Misode
d7781d717a Add 11 icons for recently added generators 2024-09-18 14:24:05 +02:00
Misode
7ea4227451 Fix #604 validate set_contents 2024-09-17 02:36:44 +02:00
Misode
98e6c9192c Remove spyglass ad 2024-09-12 03:46:37 +02:00
Misode
11896c32b1 Add new supporters 2024-09-12 03:45:59 +02:00
Misode
9c9bed9423 Fix #585 re-add enchantment loot functions to latest version 2024-09-12 03:28:02 +02:00
Misode
fff1b1603a Fix #596 support item tag and loot table references in loot preview 2024-09-11 14:39:53 +02:00
Misode
95f7ca7738 Account for deprecated.json translation info file 2024-09-11 13:49:46 +02:00
Apollo
944dc890e8 Fix recipe preview on 1.21.2, add enchantment tag generator (#595)
* Add enchantment tag generator

* Update Lithostitched partner

* Fix recipe preview on 1.21.2

* Add enchantment tag icon

* Fix nested tags

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-09-11 02:47:14 +02:00
Misode
fd6de2ac85 Update loot table preview, item display and tooltips to 1.21 2024-09-11 02:31:17 +02:00
Misode
337b7d9b0a Fix #546 catch invalid recipe resource locations in result 2024-09-05 15:10:07 +02:00
Misode
ee91810612 Fix sequence function for older versions 2024-09-05 14:11:01 +02:00
Misode
a710923f12 Fix #589 Remove conditions field from sequence loot function 2024-09-05 13:58:33 +02:00
Misode
711cec5c38 Fix #590 add replace field to set_attributes 2024-09-05 12:50:02 +02:00
Misode
6ce58a1be1 Update to 24w36a, thanks Apollo! 2024-09-04 23:46:39 +02:00
Misode
0e282bd832 Add support for 1.21.2 (24w33a) 2024-08-17 21:26:13 +02:00
Misode
60d59e9f34 Don't allow nested loot functions in sequence 2024-08-14 21:01:34 +02:00
Misode
7d200abed1 Fix #577 inline instruments and jukebox songs in components 2024-08-14 20:41:41 +02:00
Misode
de16650ce0 Add new zh-cn translations from #559
Authored by https://github.com/fuuuuuuuuuuuuuuck
2024-08-14 19:13:40 +02:00
Misode
0a240f41d3 Fix #561 add sequence loot function 2024-08-14 19:05:12 +02:00
Misode
28dab5f0ca Fix #558 add show_notification recipe field 2024-08-14 18:39:57 +02:00
Misode
795826bfc2 Fix #579 pack format for 1.21 is 48 2024-08-14 18:20:00 +02:00
Misode
9ad3e91893 Add spyglass ad 2024-07-09 01:58:00 +02:00
Misode
c277880a01 Add snbt output format 2024-06-22 16:52:01 +02:00
Misode
074790cf6b Make 1.21 the default version 2024-06-22 16:34:36 +02:00
Misode
acaece0984 Bump versions 2024-06-14 05:56:06 +02:00
Apollo
f478813a46 Update Lithostitched partner (#539)
* Add enchantment tag generator

* Update Lithostitched partner

* fix config.json

* Refactor a bit

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-06-14 05:54:10 +02:00
Misode
86b3bed22a Fix #502, fix #516, fix #529, fix #530, fix #531 2024-06-14 05:48:16 +02:00
Misode
d849fca07e Change lagacy folder names for 1.21 2024-05-28 23:40:39 +02:00
Misode
50ae97207d 24w21a 2024-05-28 22:40:57 +02:00
Misode
22207676f9 Fix #520 lodestone tracker component 2024-05-16 21:38:10 +02:00
Misode
08e5a91909 Dimension padding can also be a single value 2024-05-15 18:46:37 +02:00
Misode
934c12dc22 24w20a + fix enchantment tags 2024-05-15 17:35:13 +02:00
Misode
9612cbd97f Update mcschema with locales 2024-05-11 04:00:05 +02:00
Misode
44b97e357f 24w19a 2024-05-10 23:15:41 +02:00
Misode
bf36ecd3e0 Fix 1.20.5 should be the default 2024-05-07 20:15:32 +02:00
Misode
b3bf153d24 Add painting variant + add enchantment tag 2024-05-07 04:09:52 +02:00
Misode
9f2491833d Add body equipment slot group 2024-05-05 17:00:30 +02:00
Misode
c2340b7c05 Add particle type options + fix set_fireworks 2024-05-04 01:59:39 +02:00
Misode
a3062131a3 Add random uuid button 2024-05-03 20:34:07 +02:00
Misode
1c629a4d7f Add support for 24w18a (1.21) + make 1.20.5 default 2024-05-03 19:10:57 +02:00
Misode
42f94b04f6 Fix #506 update pack format to 41 2024-05-02 13:18:46 +02:00
Misode
4da8beb0fb Fix #504 recipe preview error 2024-04-23 20:07:09 +02:00
Misode
ec688b52f3 1.20.5-pre2 2024-04-15 18:32:16 +02:00
Misode
25bf80a05a Fix list operation dispatched fields 2024-04-13 19:38:29 +02:00
Misode
44ffd10a88 Fix #501 num_bees_inside is an int_bounds 2024-04-13 03:02:13 +02:00
Misode
99e6355530 Add set_ominous_bottle_amplifier 2024-04-10 19:38:10 +02:00
Misode
64f02c20f2 1.20.5-pre1 2024-04-10 19:13:57 +02:00
Misode
97571f7a8a Actually fix slot range collection 2024-04-08 17:54:23 +02:00
Misode
566cd52a5a Fix slot range collection 2024-04-03 01:02:14 +02:00
Misode
6f1952a5f0 Revert "Enable demo version"
This reverts commit f0d3023cfc.
2024-04-02 05:18:12 +02:00
Misode
f0d3023cfc Enable demo version 2024-04-01 01:51:38 +02:00
Misode
a693d4d119 Fix some loot table enum contexts 2024-03-31 16:03:54 +02:00
Misode
581116c67d Fix #498 remove functions field on composite entries 2024-03-31 15:55:41 +02:00
Misode
7ecd355ea3 Fix #497 attribute modifier component uuid 2024-03-30 15:30:32 +01:00
Misode
84a6da7ee8 24w13a 2024-03-28 15:40:07 +01:00
Misode
3cb736e7c5 Fix #496 recipe preview 2024-03-27 01:58:14 +01:00
Misode
3fdda11c17 Fix recipe result preview before 1.20.5 2024-03-26 00:55:37 +01:00
Misode
c87ede8e54 Recipe preview 2024-03-25 21:57:47 +01:00
Misode
1e5a4262f8 24w12a 2024-03-21 01:04:46 +01:00
Misode
497964ef1b 24w11a changes (missing locales) 2024-03-15 02:44:04 +01:00
Misode
b1332d7d22 Fix #485 add scrollPastEnd 50% to output panel 2024-03-14 17:31:18 +01:00
Misode
8195c05bc1 Fix #489 point of interest type tag URL 2024-03-13 18:52:37 +01:00
Misode
f9ad30a294 Revert reset button for map types 2024-03-12 00:56:18 +01:00
Misode
4851da4e9d Fix #482 allow resetting objects and maps on invalid type 2024-03-11 20:55:32 +01:00
Misode
9dbcf46a53 Clear map key select after adding an entry 2024-03-11 13:29:51 +01:00
Misode
16475e0ba2 Fix #487 profile component + negative component patch 2024-03-11 13:19:20 +01:00
Misode
95df45acb1 24w10a 2024-03-07 01:13:20 +01:00
Misode
1c5fce9036 Always use datalists for components 2024-03-02 03:45:09 +01:00
Misode
48e9418601 Fix map and list contexts + update schema locales 2024-03-02 03:34:11 +01:00
Misode
fb0932ef52 Fix #477 related item stack changes 2024-03-02 01:39:12 +01:00
Misode
0739a5b3ec Add 1.20.5 2024-03-01 18:14:00 +01:00
YanisBft
5bc8c90cf8 Improve French translation for generators (#471) 2024-02-24 22:18:00 +01:00
Misode
9aec6f905e Sort all locales 2024-02-24 21:59:49 +01:00
efekos
231ecfe9c2 Language Support in Tooltips (#468)
* Add lang parameter to TextCompoennt

* Add 'mclang' key

* Add clearing old language cache when changed language

* Fetch language in background

* Remove auto-fetching mc language

* wrap mclang with a state

* update contributos

* Move 'mclang' to config.json

* Cleanup code to get mclang key and fix caching

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-02-24 21:55:41 +01:00
YanisBft
7a7c7d675e Add a wiki page button to versions (#470)
* Add a wiki page button to versions

* Wrap tabs and change tab to "Wiki"

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-02-24 21:24:35 +01:00
Misode
3004ce6534 Add text component font validator 2024-02-23 03:26:06 +01:00
Misode
f7629b704d Update contributors 2024-02-23 02:44:03 +01:00
Misode
8c5328d700 Bump mcschema, various bugfixes 2024-02-23 02:08:51 +01:00
efekos
f9b17df84e Update Turkish language (#454)
* make Turkish have schemas

* Update translators

* Fix a typo

"Şablon materyali" to "Şablon Materyali"

* Add tr.json

* Remove partners translations

* Format locale file with 2 spaces

* Update mcschema locales

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-01-22 20:32:04 +01:00
Misode
9f8e57bb19 Update OTTYG minVersion 2024-01-18 07:59:05 +01:00
Misode
f3b852025d Add Oh The Trees You'll Grow partner generator 2024-01-18 07:53:05 +01:00
Apollo
f855c7d938 Add Lithostitched partner (#436)
* Add Lithostitched partner

* Remove modded biome slice stuff

* Run formatter

---------

Co-authored-by: Misode <misoloo64@gmail.com>
2024-01-18 04:11:18 +01:00
OliviaTheVampire
7c94beb5b2 More support for Obsidian (#233)
* Start obsidian item generator

* Add part of block generator

* More progress on blocks and items for Obsidian

* Fixed some issues

* Added even more properties for blocks

* fixed this?

* added back these?

* Hopefully fixed this?

* More events

* should fix the issues

* NOW WORK

* adding support for even more

* Fix build

* Rename obsidian IDs

* updated pack version

* Update config

---------

Co-authored-by: Misode <Misoloo64@gmail.com>
2024-01-18 03:38:54 +01:00
Misode
b4e9d86c43 Refactor how partner generators work + prefix generator translation keys 2024-01-18 03:22:10 +01:00
efekos
acc55ae7a3 Add Turkish language (#453) 2024-01-14 03:37:50 +01:00
notlin4
f44fd734a6 Translated using Weblate (Chinese (Traditional))
Currently translated at 50.3% (141 of 280 strings)

Translation: Misode's Data Pack Generators/Web App
Translate-URL: https://weblate.spyglassmc.com/projects/misode-github-io/web-app/zh_Hant/
2023-12-31 12:27:34 +00:00
Misode
f6feffff6e Update supporters 2023-12-29 15:41:20 +01:00
Misode
80324b2498 Fix wiki links 2023-12-29 15:03:53 +01:00
Misode
98e671b8a0 Add 1.20.4 to version name 2023-12-11 22:07:13 +01:00
157 changed files with 19741 additions and 7161 deletions

View File

@@ -68,6 +68,7 @@ module.exports = {
'quote-props': [
'warn',
'as-needed',
{ numbers: true },
],
},
}

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: 'Get latest version'
id: version
@@ -23,7 +23,7 @@ jobs:
npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v3
with:
path: ./dist
@@ -41,5 +41,5 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v4
id: deployment

12
.vscode/settings.json vendored
View File

@@ -1,20 +1,20 @@
{
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit"
}
},
"[typescriptreact]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit"
}
},
"[javascript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit"
}
},
"[markdown]": {

View File

@@ -1,5 +1,5 @@
# misode.github.io
Data Pack Generators and Guides for Minecraft Java Edition
> Data Pack Generators for Minecraft Java Edition
https://misode.github.io/
@@ -16,10 +16,41 @@ npm run dev
## Translating
misode.github.io supports multiple languages. If you'd like to help us translate this project to your language, it would be really appreciated! If your language is not on this list, please create an issue for it.
[![Localization status](https://l10n.spgoding.com/widgets/minecraft-schemas/-/multi-auto.svg)](https://l10n.spgoding.com/engage/minecraft-schemas/?utm_source=widget)
[![Localization status](https://weblate.spyglassmc.com/widgets/misode-github-io/-/multi-auto.svg)](https://weblate.spyglassmc.com/engage/misode-github-io/?utm_source=widget)
1. Go to the [localization website](https://l10n.spgoding.com) (hosted by [SPGoding](https://github.com/SPGoding)).
1. Go to the [Spyglassmc localization website](https://weblate.spyglassmc.com/projects/) (hosted by [SPGoding](https://github.com/SPGoding)).
2. [Register](https://l10n.spgoding.com/accounts/register) by linking your GitHub account (recommended), or using your email.
- Note that the username and email will be shown in the [repository](https://github.com/misode/misode.github.io)'s git commit log.
3. See the components of misode.github.io [here](https://l10n.spgoding.com/projects/minecraft-schemas/).
3. See the components of misode.github.io [here](https://weblate.spyglassmc.com/projects/misode-github-io/web-app/).
4. Start translating!
## Modded Generators
This website contains a few [non-vanilla generators](https://misode.github.io/predicate/). It is possible to contribute additional generators. If instead you are interested in making custom generators but don't want them part of the main website, see the [forking section](#forking) below.
1. Create a new file `public/mcdoc/<your_project>.mcdoc`. This will contain the definitions of the
2. Create a new generator entry in the `src/app/config.json` file for each generator page that you want to add. Set its `dependency` field to the name of the mcdoc file you created.
3. Add translation key for each generator in `src/locales/en.json`, named `generator.<id>`, and a translation key named `partner.<dependency>`.
4. The final step will be to write the generator definitions in the mcdoc file. Apart from the [technical specification](https://spyglassmc.com/user/mcdoc/), there is no documentation for the mcdoc format. It is a custom language describing JSON and NBT structures in the game. I recommend taking a look at how the other modded generators have their types. You can also look at the [vanilla mcdoc definitions](https://github.com/SpyglassMC/vanilla-mcdoc).
5. Feel free to open a PR even when you are not ready with the types, or if you want help with writing them.
## Forking
You are allowed to fork this repository and use its base as a way to publish your own generator site, but I ask to make a few changes before publishing.
1. Change links to this repo to your own repo. This can be done at the top of `Utils.ts` by changing `export const SOURCE_REPO_URL = ...`.
2. Remove or replace the Google Analytics tracking code in the root `index.html` file. To avoid breaking the rest of the website, you can replace everything between the `<!-- Global site tag (gtag.js) - Google Analytics -->` markers with this:
```html
<script>
function gtag() {}
</script>
```
3. Disable the ads, first by remove two lines in `index.html`:
```html
<script async src="https://media.ethicalads.io/media/client/ethicalads.min.js"></script>
...
<div data-ea-publisher="misode-github-io" data-ea-manual="true" id="ad-placeholder"></div>
```
4. Secondly, you can remove the ad component, for example by returning `<></>` in `Ad.tsx`, or by removing the `{!gen.tags?.includes('partners') && <Ad id="data-pack-generator" type="text" />}` line in `SchemaGenerator.tsx`.
5. Remove the contributors and giscus comment section on the homepage. You can do this easily by removing `<Contributors />` and `<Giscus />` in `Home.tsx`.
6. Make some other changes to the home page. This will depend on what you need, but you might want to remove stuff like `<WhatsNew />` and/or `<Tools />`.
7. Edit the `Footer.tsx` component. You can remove the donation link, but I would appreciate if you still kept a note that your fork is based on my work, for example by linking to my github profile or this repository.
8. Change some of the translations in `src/locales/en.json`. Particularly you might want to change the `title.home` key.

View File

@@ -8,29 +8,16 @@
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-S982VZS08T', {
send_page_view: false,
theme: localStorage.getItem('theme') || 'default',
version: localStorage.getItem('schema_version') || '1.20.3',
version: localStorage.getItem('schema_version') || '1.21.9',
locale: localStorage.getItem('language') || 'en',
prefers_color_scheme: matchMedia('(prefers-color-scheme: light)').matches ? 'light' : matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'none',
tree_view_mode: localStorage.getItem('misode_tree_view_mode') || 'default',
colormap: localStorage.getItem('misode_colormap') || 'default',
});
</script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-73024255-2', 'auto');
ga('set', 'page', location.pathname);
ga('set', 'dimension1', localStorage.getItem('theme') || 'default');
ga('set', 'dimension2', 'v2');
ga('set', 'dimension3', localStorage.getItem('schema_version') || '1.20.3');
ga('set', 'dimension4', localStorage.getItem('language') || 'en');
ga('set', 'dimension5', 'none');
ga('set', 'dimension7', matchMedia('(prefers-color-scheme: light)').matches ? 'light' : matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'none');
ga('send', 'pageview');
</script>
<!-- End: Global site tag (gtag.js) - Google Analytics -->
<script>
(() => {
const theme = localStorage.getItem('theme')
@@ -42,7 +29,7 @@
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Pack Generators - Minecraft 1.18, 1.19, 1.20</title>
<title>Data Pack Generators - Minecraft 1.19, 1.20, 1.21</title>
<link rel="icon" href="/src/favicon-32.png" sizes="32x32">
<script async src="https://media.ethicalads.io/media/client/ethicalads.min.js"></script>
<script>

5228
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,37 +16,35 @@
"license": "MIT",
"dependencies": {
"@giscus/react": "^2.2.3",
"@mcschema/core": "^0.12.40",
"@mcschema/java-1.15": "^0.2.7",
"@mcschema/java-1.16": "^0.6.14",
"@mcschema/java-1.17": "^0.2.33",
"@mcschema/java-1.18": "^0.3.9",
"@mcschema/java-1.18.2": "^0.1.18",
"@mcschema/java-1.19": "^0.1.45",
"@mcschema/java-1.19.3": "^0.0.8",
"@mcschema/java-1.19.4": "^0.1.11",
"@mcschema/java-1.20": "^0.0.14",
"@mcschema/java-1.20.2": "^0.0.4",
"@mcschema/java-1.20.3": "^0.0.3",
"@mcschema/locales": "^0.1.92",
"@spyglassmc/core": "^0.4.43",
"@spyglassmc/java-edition": "^0.3.55",
"@spyglassmc/json": "^0.3.47",
"@spyglassmc/locales": "^0.3.22",
"@spyglassmc/mcdoc": "^0.3.47",
"@spyglassmc/nbt": "^0.3.49",
"@zip.js/zip.js": "^2.4.5",
"brace": "^0.11.1",
"buffer": "^6.0.3",
"comment-json": "^4.1.1",
"deepslate": "^0.18.0",
"deepslate-1.18": "npm:deepslate@^0.9.0-beta.9",
"deepslate-1.18.2": "npm:deepslate@^0.9.0-beta.13",
"deepslate": "^0.25.1",
"deepslate-1.18": "npm:deepslate@0.9.0-beta.9",
"deepslate-1.18.2": "npm:deepslate@0.9.0",
"deepslate-1.20.4": "npm:deepslate@0.20.1",
"diff": "^7.0.0",
"dompurify": "^3.2.6",
"highlight.js": "^11.5.1",
"howler": "^2.2.3",
"js-yaml": "^3.14.1",
"lz-string": "^1.4.4",
"marked": "^4.0.10",
"rfdc": "^1.3.0",
"sourcemapped-stacktrace": "^1.1.11"
"sourcemapped-stacktrace": "^1.1.11",
"spark-md5": "^3.0.2",
"vscode-languageserver-textdocument": "^1.0.12"
},
"devDependencies": {
"@preact/preset-vite": "^2.4.0",
"@preact/preset-vite": "^2.10.0",
"@rollup/plugin-html": "^1.0.1",
"@types/diff": "^5.2.2",
"@types/google.analytics": "0.0.40",
"@types/gtag.js": "^0.0.10",
"@types/howler": "^2.2.4",
@@ -54,6 +52,7 @@
"@types/lz-string": "^1.3.34",
"@types/marked": "^4.0.1",
"@types/seedrandom": "^2.4.28",
"@types/spark-md5": "^3.0.5",
"@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.28.0",
"autoprefixer": "^10.4.16",
@@ -64,7 +63,7 @@
"rollup-plugin-visualizer": "^5.6.0",
"tailwindcss": "^3.3.3",
"typescript": "^4.7.3",
"vite": "^3.2.7",
"vite-plugin-static-copy": "^0.12.0"
"vite": "^6.0.11",
"vite-plugin-static-copy": "^2.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

BIN
public/images/furnace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

BIN
public/images/smithing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 B

View File

@@ -0,0 +1,19 @@
dispatch minecraft:resource[ad_astra:planets] to struct AdAstraPlanet {
/// The dimension id for this planet to use
dimension: #[id="dimension"] string,
/// Due to floating point precision, the actual gravity may be slightly off in-game
gravity: float @ 0..,
/// The orbit dimension id. If left blank, this planet is treated as an orbit dimension
orbit?: #[id="dimension"] string,
oxygen: boolean,
solar_power: int @ 1..,
/// Controls where the planet will be on the selector screen
solar_system: #[id="dimension"] string,
/// Below -50 will freeze, above 70 will burn
temperature: int,
/// The minimum rocket tier required to reach this planet
tier: int @ 1..,
/// Additional dimensions that the player can launch from
additional_launch_dimensions: [#[id="dimension"] string]
}

252
public/mcdoc/create.mcdoc Normal file
View File

@@ -0,0 +1,252 @@
use ::java::world::component::DataComponentPatch
dispatch minecraft:resource[create:recipes] to struct Recipes {
type: Type,
...create:recipes[[type]],
}
enum(string) Type {
Compacting = "create:compacting",
Crushing = "create:crushing",
Cutting = "create:cutting",
Deploying = "create:deploying",
Emptying = "create:emptying",
Filling = "create:filling",
Haunting = "create:haunting",
ItemApplication = "create:item_application",
MechanicalCrafting = "create:mechanical_crafting",
Milling = "create:milling",
Mixing = "create:mixing",
Pressing = "create:pressing",
SandpaperPolishing = "create:sandpaper_polishing",
SequencedAssembly = "create:sequenced_assembly",
Splashing = "create:splashing",
}
struct NBT {
Bottle?: ("REGULAR" | "SPLASH" | "LINGERING"),
Potion?: string,
}
type Item = struct {
#[until="1.21.1"]
item: string,
#[since="1.21.1"]
id: string,
chance?: float @ 0..,
count?: int @ 1..,
#[since="1.21.1"]
components?: DataComponentPatch,
}
type ItemWithCount = struct {
#[until="1.21.1"]
item: string,
#[since="1.21.1"]
id: string,
count?: int @ 0..,
#[since="1.21.1"]
components?: DataComponentPatch,
}
type SimpleItem = struct {
#[until="1.21.1"]
item: string,
#[since="1.21.1"]
id: string,
#[since="1.21.1"]
components?: DataComponentPatch,
}
type Fluid = struct {
#[until="1.21.1"]
fluid: string,
#[until="1.21.1"]
nbt?: NBT,
#[since="1.21.1"]
id: string,
amount: int @ 1..,
#[since="1.21.1"]
components?: DataComponentPatch,
}
type ItemOrTag = (
struct {
item: string,
} | struct {
tag: string,
}
)
type ItemOrTagWithCount = (
struct {
item: string,
count?: int @ 1..,
} | struct {
tag: string,
count?: int @ 1..,
}
)
type FluidOrTag = (
struct {
fluid: string,
#[since="1.21.1"]
type: "fluid_stack",
amount: int @ 1..,
#[until="1.21.1"]
nbt?: NBT,
#[since="1.21.1"]
components?: DataComponentPatch,
} | struct {
#[until="1.21.1"]
fluidTag: string,
#[since="1.21.1"]
fluid_tag: string,
#[since="1.21.1"]
type: "fluid_tag",
amount: int @ 1..,
#[until="1.21.1"]
nbt?: NBT,
#[since="1.21.1"]
components?: DataComponentPatch,
}
)
type MixingResult = struct {
id: string,
/// Used for items; optional field.
count?: int @ 1..,
/// Used for fluids; mandatory field.
amount?: int @ 1..,
components?: DataComponentPatch,
}
dispatch create:recipes[create:compacting] to struct {
ingredients: [(ItemOrTagWithCount | FluidOrTag)] @ 1..,
results: (
#[until="1.21.1"]
[(ItemWithCount | Fluid)] @ 1.. |
#[since="1.21.1"]
[MixingResult] @ 1 |
),
}
dispatch create:recipes[create:crushing] to struct {
#[until="1.21.1"]
processingTime: int @ 1..,
#[since="1.21.1"]
processing_time: int @ 1..,
ingredients: [ItemOrTag] @ 1,
results: [Item] @ 1..,
}
dispatch create:recipes[create:cutting] to struct {
#[until="1.21.1"]
processingTime: int @ 1..,
#[since="1.21.1"]
processing_time: int @ 1..,
ingredients: [ItemOrTag] @ 1,
results: [Item] @ 1,
}
dispatch create:recipes[create:deploying] to struct {
/// The first item is the base item; the second is the ingredient to be deployed.
ingredients: [ItemOrTag] @ 2,
/// Defaults to false.
#[until="1.21.1"]
keepHeldItem?: boolean,
/// Defaults to false.
#[since="1.21.1"]
keep_held_item?: boolean,
results: [SimpleItem] @ 1,
}
dispatch create:recipes[create:emptying] to struct {
ingredients: [ItemOrTag] @ 1,
results: [SimpleItem, Fluid],
}
dispatch create:recipes[create:filling] to struct {
ingredients: [ItemOrTag, FluidOrTag],
results: [SimpleItem] @ 1,
}
dispatch create:recipes[create:haunting] to struct {
ingredients: [ItemOrTag] @ 1,
results: [Item] @ 1..,
}
dispatch create:recipes[create:item_application] to struct {
/// The first item is the base item; the second is the ingredient to be applied.
ingredients: [ItemOrTag] @ 2,
results: [SimpleItem] @ 1,
}
dispatch create:recipes[create:mechanical_crafting] to struct {
#[until="1.21.1"]
acceptMirrored: boolean,
#[since="1.21.1"]
accept_mirrored: boolean,
/// Identifier for the category this goes in the recipe book.
#[since="1.21.1"]
category: string,
/// **Warning:** Recipes larger than 9x9 will not be displayed in JEI.
pattern: [#[crafting_ingredient(definition=true)] string],
key: struct {
[#[crafting_ingredient] string]: ItemOrTag,
},
result: ItemWithCount,
/// Determines if a notification is shown when unlocking this recipe. Defaults to true.
#[since="1.21.1"]
show_notification?: boolean,
}
dispatch create:recipes[create:milling] to struct {
#[until="1.21.1"]
processingTime: int @ 1..,
#[since="1.21.1"]
processing_time: int @ 1..,
ingredients: [ItemOrTag] @ 1,
results: [Item] @ 1..,
}
dispatch create:recipes[create:mixing] to struct {
#[until="1.21.1"]
heatRequirement?: ("heated" | "superheated"),
#[since="1.21.1"]
heat_requirement?: ("heated" | "superheated"),
ingredients: [(ItemOrTagWithCount | FluidOrTag)] @ 1..,
results: (
#[until="1.21.1"]
[(ItemWithCount | Fluid)] @ 1.. |
#[since="1.21.1"]
[MixingResult] @ 1 |
),
}
dispatch create:recipes[create:pressing] to struct {
ingredients: [ItemOrTag] @ 1..,
results: [ItemWithCount] @ 1,
}
dispatch create:recipes[create:sandpaper_polishing] to struct {
ingredients: [ItemOrTag] @ 1,
results: [ItemWithCount] @ 1,
}
dispatch create:recipes[create:sequenced_assembly] to struct {
ingredient: ItemOrTag,
loops: int @ 1..,
results: [Item] @ 1..,
sequence: [Recipes],
#[until="1.21.1"]
transitionalItem: SimpleItem,
#[since="1.21.1"]
transitional_item: SimpleItem,
}
dispatch create:recipes[create:splashing] to struct {
ingredients: [ItemOrTag] @ 1,
results: [Item] @ 1..,
}

168
public/mcdoc/fabric.mcdoc Normal file
View File

@@ -0,0 +1,168 @@
// Sources:
// - https://wiki.fabricmc.net/documentation:fabric_mod_json
// - https://wiki.fabricmc.net/documentation:fabric_mod_json_spec
// - https://github.com/FabricMC/fabric-loader/blob/master/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java
dispatch minecraft:resource[fabric:fabric_mod_json] to struct FabricModJson {
/// Needed for internal mechanisms. Must always be `1`.
schemaVersion: 1,
/// The mod's identifier of Latin letters, digits, or underscores.
id: string,
/// The mod's version. Optionally matching the [Semantic Versioning 2.0.0](https://semver.org/) specification.
version: string @ 1..,
/// User-friendly mod's name. Defaults to `id`.
name?: string,
/// The mod's description. Defaults to an empty string.
description?: string,
/// Authors of the mod.
authors?: People,
/// Contributors to the mod.
contributors?: People,
/// Contact information for the project
contact?: ContactInfo,
/// Licensing information.
/// Should provide the complete set of preferred licenses conveying the entire mod package. In other words, compliance with all listed licenses should be sufficient for usage, redistribution, etc. of the mod package as a whole.
/// For cases where a part of code is dual-licensed, choose the preferred license. The list is not exhaustive, serves primarily as a kind of hint, and does not prevent you from granting additional rights/licenses on a case-by-case basis.
/// To aid automated tools, it is recommended to use [SPDX License Identifiers](https://spdx.org/licenses/) for open-source licenses.
license?: License,
/// The mod's icon file. Should be a square PNG image.
/// Resource packs use 128×128, but that is not a hard requirement. A power of two is recommended.
/// Can also be provided as a dictionary of images widths to their file paths.
icon?: Icon,
/// Defines the list of ids of the mod. It can be seen as the aliases of the mod.
/// Fabric Loader will treat these ids as mods that exist.
/// If there are other mods using that id, they will not be loaded.
provides?: [string],
/// Defines where the mod runs: only on the client side (client mod), only on the dedicated server side (plugin) or on both sides (regular mod).
environment?: EnvironmentType,
/// Main classes of the mod that will be loaded.
entrypoints?: Entrypoints,
/// Nested JARs inside your mod's JAR to load.
jars?: [Jar],
/// Adapters for used languages to their adapter classes full names.
languageAdapters?: LanguageAdapters,
/// List of mixin configuration files. Each entry is the path to the mixin configuration file inside your mod's JAR.
mixins?: Mixins,
/// Access widener configuration file.
accessWidener?: string,
/// Dependencies required to run. Without them a game will crash.
depends?: Dependencies,
/// Dependencies not required to run. Without them a game will log a warning.
recommends?: Dependencies,
/// Dependencies not required to run. Use this as a kind of metadata.
suggests?: Dependencies,
/// Mods whose together with yours might cause a game crash. With them a game will crash.
breaks?: Dependencies,
/// Mods whose together with yours cause some kind of bugs, etc. With them a game will log a warning.
conflicts?: Dependencies,
/// Custom fields. It is recommended to namespace fields to avoid conflicts.
custom?: CustomValues,
}
enum(string) EnvironmentType {
Universal = "*",
Client = "client",
Server = "server",
}
struct Entrypoints {
[string]: [(string | Entrypoint)],
}
struct Entrypoint {
value: string,
/// Defaults to `default`.
adapter?: string,
}
struct Jar {
/// Path inside your mod's JAR to the nested JAR.
file: string,
}
type Mixins = [(string | Mixin)]
struct Mixin {
/// The path to the mixin configuration file inside your mod's JAR.
config: string,
/// Defaults to universal (`*`).
environment?: EnvironmentType,
}
struct Dependencies {
[string]: (string | [string]),
}
type People = [(string | Person)]
struct Person {
/// The real name, or username of the person.
name: string,
/// The person's contact information.
contact?: ContactInfo,
}
struct ContactInfo {
/// Contact e-mail address.
email?: #[email] string,
/// IRC channel.
irc?: string,
/// Link to the project or user homepage.
homepage?: #[url] string,
/// Link to the project's issue tracker.
issues?: #[url] string,
/// Link to the project's source code repository
sources?: string,
[string]: string,
}
type License = (string | [string])
type Icon = (string | IconMap)
struct IconMap {
[#[integer(min=1)] string]: string,
}
struct LanguageAdapters {
[string]: string,
}
struct CustomValues {
[string]: any,
}
// Sources:
// - https://wiki.fabricmc.net/tutorial:dependency_overrides
// - https://github.com/FabricMC/fabric-loader/blob/master/src/main/java/net/fabricmc/loader/impl/metadata/DependencyOverrides.java
dispatch minecraft:resource[fabric:dependency_overrides] to struct DependencyOverrides {
/// Needed for internal mechanisms. Must always be `1`.
version: 1,
overrides: Overrides,
}
struct Overrides {
[string]: Override,
}
struct Override {
[DependencyType]: Dependencies,
}
enum(string) DependencyType {
ReplaceDepends = "depends",
ReplaceRecommends = "recommends",
ReplaceSuggests = "suggests",
ReplaceConflicts = "conflicts",
ReplaceBreaks = "breaks",
AddDepends = "+depends",
AddRecommends = "+recommends",
AddSuggests = "+suggests",
AddConflicts = "+conflicts",
AddBreaks = "+breaks",
RemoveDepends = "-depends",
RemoveRecommends = "-recommends",
RemoveSuggests = "-suggests",
RemoveConflicts = "-conflicts",
RemoveBreaks = "-breaks",
}

View File

@@ -0,0 +1,116 @@
use ::java::util::direction::Direction
use ::java::util::block_state::BlockState
use ::java::data::worldgen::biome::Precipitation
use ::java::data::worldgen::processor_list::BlockMatch
use ::java::data::worldgen::processor_list::BlockStateMatch
use ::java::data::worldgen::processor_list::RandomBlockMatch
use ::java::data::worldgen::processor_list::RandomBlockStateMatch
use ::java::data::worldgen::processor_list::TagMatch
dispatch minecraft:resource[immersive_weathering:block_growth] to struct BlockGrowth {
area_condition: AreaCondition,
position_predicates?: [PositionTest],
growth_chance: float @ 0..1,
growth_for_face: [GrowthFace],
owners: [#[id="block"] string],
replacing_target: RuleTest,
target_self?: boolean,
destroy_target?: boolean,
}
struct GrowthFace {
direction?: Direction,
weight?: int,
growth: [struct {
weight: int,
data: BlockPair,
}],
}
struct BlockPair {
block: BlockState,
above_block?: BlockState,
}
struct AreaCondition {
type: ("generate_if_not_too_many" | "neighbor_based_generation"),
...immersive_weathering:area_condition[[type]],
}
dispatch immersive_weathering:area_condition[generate_if_not_too_many] to struct GenerateIfNotTooMany {
radiusX: int,
radiusY: int,
radiusZ: int,
requiredAmount: int,
yOffset?: int,
must_have?: RuleTest,
must_not_have?: RuleTest,
includes?: (#[id(registry="block",tags="allowed")] string | [#[id="block"] string]),
}
dispatch immersive_weathering:area_condition[neighbor_based_generation] to struct NeighborBasedGeneration {
must_have: RuleTest,
must_not_have?: RuleTest,
required_amount?: int,
directions: [Direction],
}
struct PositionTest {
type: ("biome_match" | "day_test" | "nand" | "precipitation_test" | "temperature_range"),
...immersive_weathering:position_test[[type]],
}
dispatch immersive_weathering:position_test[biome_match] to struct BiomeMatch {
biomes: (#[id(registry="worldgen/biome",tags="allowed")] string | [#[id="worldgen/biome"] string]),
}
dispatch immersive_weathering:position_test[day_test] to struct DayTest {
day: boolean,
}
dispatch immersive_weathering:position_test[nand] to struct Nand {
predicates: [PositionTest],
}
dispatch immersive_weathering:position_test[precipitation_test] to struct PrecipitationTest {
precipitation: Precipitation,
}
dispatch immersive_weathering:position_test[temperature_range] to struct TemperatureRange {
min: float,
max: float,
use_local_pos?: boolean,
}
struct RuleTest {
predicate_type: #[id] RuleTestType,
...immersive_weathering:rule_test[[predicate_type]],
}
enum(string) RuleTestType {
#[starred] BlockSetMatch = "immersive_weathering:block_set_match",
#[starred] FluidMatch = "immersive_weathering:fluid_match",
#[starred] TreeLog = "immersive_weathering:tree_log",
BlockMatch = "block_match",
BlockStateMatch = "blockstate_match",
RandomBlockMatch = "random_block_match",
RandomBlockStateMatch = "random_blockstate_match",
TagMatch = "tag_match",
}
dispatch immersive_weathering:rule_test[block_match] to BlockMatch
dispatch immersive_weathering:rule_test[blockstate_match] to BlockStateMatch
dispatch immersive_weathering:rule_test[random_block_match] to RandomBlockMatch
dispatch immersive_weathering:rule_test[random_blockstate_match] to RandomBlockStateMatch
dispatch immersive_weathering:rule_test[tag_match] to TagMatch
dispatch immersive_weathering:rule_test[immersive_weathering:block_set_match] to struct BlockSetMatch {
blocks: (#[id(registry="block",tags="allowed")] string | [#[id="block"] string]),
probability?: float @ 0..1,
}
dispatch immersive_weathering:rule_test[immersive_weathering:fluid_match] to struct FluidMatch {
fluids: #[id="fluid"] string,
}
dispatch immersive_weathering:rule_test[immersive_weathering:tree_log] to struct {}

View File

@@ -0,0 +1,113 @@
use ::java::data::loot::LootPool
use ::java::data::loot::LootPoolEntry
use ::java::data::loot::LootContextType
use ::java::data::loot::LootCondition
dispatch minecraft:resource[loot-table-modifier:loot_modifier] to struct {
actions: [Action],
predicate: Predicate,
}
struct Action {
type: #[id] ActionType,
...loot-table-modifier:loot_modifier_action_types[[type]],
}
struct Predicate {
type: #[id] PredicateType,
...loot-table-modifier:loot_modifier_predicate_types[[type]],
}
enum(string) ActionType {
PoolAdd = "loot-table-modifier:pool_add",
PoolRemove = "loot-table-modifier:pool_remove",
EntryAdd = "loot-table-modifier:entry_add",
EntryRemove = "loot-table-modifier:entry_remove",
EntryItemSet = "loot-table-modifier:entry_item_set",
ConditionAdd = "loot-table-modifier:condition_add"
}
enum(string) PredicateType {
Inverted = "loot-table-modifier:inverted",
AnyOf = "loot-table-modifier:any_of",
AllOf = "loot-table-modifier:all_of",
EntryItem = "loot-table-modifier:entry_item",
Table = "loot-table-modifier:table",
}
/// Utils
struct Pattern {
regexPattern: #[regex_pattern] string,
}
type LiteralOrPattern<T> = (
#[misode_member_name="Literal"] T |
#[misode_member_name="Regex pattern"] Pattern |
)
/// Actions
dispatch loot-table-modifier:loot_modifier_action_types[loot-table-modifier:pool_add] to struct {
pools: [LootPool],
}
dispatch loot-table-modifier:loot_modifier_action_types[loot-table-modifier:pool_remove] to struct {
}
dispatch loot-table-modifier:loot_modifier_action_types[loot-table-modifier:entry_add] to struct {
entries: [LootPoolEntry],
}
dispatch loot-table-modifier:loot_modifier_action_types[loot-table-modifier:entry_remove] to struct {
}
dispatch loot-table-modifier:loot_modifier_action_types[loot-table-modifier:entry_item_set] to struct {
name: #[id="item"] string,
canReplaceEntry?: boolean,
}
dispatch loot-table-modifier:loot_modifier_action_types[loot-table-modifier:condition_add] to struct {
conditions: [LootCondition],
includePools?: boolean,
includeEntries?: boolean,
}
/// Predicates
/// # op
dispatch loot-table-modifier:loot_modifier_predicate_types[loot-table-modifier:inverted] to struct {
term: Predicate,
}
dispatch loot-table-modifier:loot_modifier_predicate_types[loot-table-modifier:any_of] to struct {
terms: [Predicate],
}
dispatch loot-table-modifier:loot_modifier_predicate_types[loot-table-modifier:all_of] to struct {
terms: [Predicate],
}
/// # entry
dispatch loot-table-modifier:loot_modifier_predicate_types[loot-table-modifier:entry_item] to struct {
name: LiteralOrPattern<#[id="item"] string>,
}
/// # table
dispatch loot-table-modifier:loot_modifier_predicate_types[loot-table-modifier:table] to struct {
identifiers?: [LiteralOrPattern<#[id="loot_table"] string>],
types?: [LiteralOrPattern<LootContextType>],
}

202
public/mcdoc/neoforge.mcdoc Normal file
View File

@@ -0,0 +1,202 @@
use ::java::data::worldgen::DecorationStep
use ::java::data::worldgen::CarveStep
use ::java::data::worldgen::biome::SpawnerData
use ::java::data::worldgen::biome::MobSpawnCost
use ::java::data::worldgen::biome::MobCategory
dispatch minecraft:resource[neoforge:biome_modifier] to struct BiomeModifier {
type: #[id] BiomeModifierType,
...neoforge:biome_modifier[[type]],
}
enum(string) BiomeModifierType {
None = "neoforge:none",
AddFeatures = "neoforge:add_features",
RemoveFeatures = "neoforge:remove_features",
AddSpawns = "neoforge:add_spawns",
RemoveSpawns = "neoforge:remove_spawns",
AddCarvers = "neoforge:add_carvers",
RemoveCarvers = "neoforge:remove_carvers",
AddSpawnCosts = "neoforge:add_spawn_costs",
RemoveSpawnCosts = "neoforge:remove_spawn_costs",
}
dispatch neoforge:biome_modifier[neoforge:none] to struct {}
struct BiomeModifierBase {
biomes: (#[id(registry="worldgen/biome",tags="allowed")] string | [#[id="worldgen/biome"] string]),
}
dispatch neoforge:biome_modifier[neoforge:add_features] to struct AddFeatures {
...BiomeModifierBase,
features: (#[id(registry="worldgen/placed_feature",tags="allowed")] string | [#[id="worldgen/placed_feature"] string]),
step: DecorationStep,
}
dispatch neoforge:biome_modifier[neoforge:remove_features] to struct RemoveFeatures {
...BiomeModifierBase,
features: (#[id(registry="worldgen/placed_feature",tags="allowed")] string | [#[id="worldgen/placed_feature"] string]),
steps: (DecorationStep | [DecorationStep]),
}
dispatch neoforge:biome_modifier[neoforge:add_spawns] to struct AddSpawns {
...BiomeModifierBase,
spawners: (SpawnerData | [SpawnerData]),
}
dispatch neoforge:biome_modifier[neoforge:remove_spawns] to struct RemoveSpawns {
...BiomeModifierBase,
entity_types: (#[id(registry="entity_type",tags="allowed")] string | [#[id="entity_type"] string]),
}
dispatch neoforge:biome_modifier[neoforge:add_carvers] to struct AddCarvers {
...BiomeModifierBase,
carvers: (#[id(registry="worldgen/configured_carver",tags="allowed")] string | [#[id="worldgen/configured_carver"] string]),
#[until="1.21.2"]
step: CarveStep,
}
dispatch neoforge:biome_modifier[neoforge:remove_carvers] to struct RemoveCarvers {
...BiomeModifierBase,
carvers: (#[id(registry="worldgen/configured_carver",tags="allowed")] string | [#[id="worldgen/configured_carver"] string]),
#[until="1.21.2"]
step: (CarveStep | [CarveStep]),
}
dispatch neoforge:biome_modifier[neoforge:add_spawn_costs] to struct AddSpawnCosts {
...BiomeModifierBase,
entity_types: (#[id(registry="entity_type",tags="allowed")] string | [#[id="entity_type"] string]),
spawn_cost: MobSpawnCost,
}
dispatch neoforge:biome_modifier[neoforge:remove_spawn_costs] to struct RemoveSpawnCosts {
...BiomeModifierBase,
entity_types: (#[id(registry="entity_type",tags="allowed")] string | [#[id="entity_type"] string]),
}
dispatch minecraft:resource[neoforge:structure_modifier] to struct StructureModifier {
type: #[id] StructureModifierType,
...neoforge:structure_modifier[[type]],
}
enum(string) StructureModifierType {
None = "neoforge:none",
AddSpawns = "neoforge:add_spawns",
RemoveSpawns = "neoforge:remove_spawns",
ClearSpawns = "neoforge:clear_spawns",
}
dispatch neoforge:structure_modifier[neoforge:none] to struct {}
struct StructureModifierBase {
structures: (#[id(registry="worldgen/structure",tags="allowed")] string | [#[id="worldgen/structure"] string]),
}
dispatch neoforge:structure_modifier[neoforge:add_spawns] to struct AddStructureSpawns {
...StructureModifierBase,
spawners: (SpawnerData | [SpawnerData]),
}
dispatch neoforge:structure_modifier[neoforge:remove_spawns] to struct RemoveStructureSpawns {
...StructureModifierBase,
entity_types: (#[id(registry="entity_type",tags="allowed")] string | [#[id="entity_type"] string]),
}
dispatch neoforge:structure_modifier[neoforge:clear_spawns] to struct ClearStructureSpawns {
...StructureModifierBase,
categories: (MobCategory | [MobCategory]),
}
type DataMap<K, V> = struct {
replace?: boolean,
values: struct DataMapValues {
[K]: (
V |
struct ReplaceableValue {
replace?: boolean,
value: V,
} |
)
},
remove?: [K],
}
dispatch minecraft:resource[neoforge:data_map_acceptable_villager_distances] to DataMap<#[id(registry="entity_type", tags="allowed")] string, (
float |
struct AcceptableVillagerDistance {
acceptable_villager_distance: float,
} |
)>
dispatch minecraft:resource[neoforge:data_map_compostables] to DataMap<#[id(registry="item",tags="allowed")] string, (
float @ 0..1 |
struct Compostable {
chance: float @ 0..1,
can_villager_compost?: boolean,
} |
)>
dispatch minecraft:resource[neoforge:data_map_furnace_fuels] to DataMap<#[id(registry="item", tags="allowed")] string, (
int @ 1.. |
struct FurnaceFuel {
burn_time: int @ 1..,
} |
)>
dispatch minecraft:resource[neoforge:data_map_monster_room_mobs] to DataMap<#[id(registry="entity_type",tags="allowed")] string, (
int @ 0.. |
struct MonsterRoomMob {
weight: int @ 0..,
} |
)>
dispatch minecraft:resource[neoforge:data_map_oxidizables] to DataMap<#[id(registry="block",tags="allowed")] string, (
#[id="block"] string |
struct Oxidizable {
next_oxidation_stage: #[id="block"] string,
} |
)>
dispatch minecraft:resource[neoforge:data_map_parrot_imitations] to DataMap<#[id(registry="entity_type",tags="allowed")] string, (
#[id="sound_event"] string |
struct ParrotImitation {
sound: #[id="sound_event"] string,
} |
)>
dispatch minecraft:resource[neoforge:data_map_raid_hero_gifts] to DataMap<#[id(registry="villager_profession",tags="allowed")] string, (
#[id="loot_table"] string |
struct RaidHeroGift {
loot_table: #[id="loot_table"] string,
} |
)>
dispatch minecraft:resource[neoforge:data_map_strippables] to DataMap<#[id(registry="block",tags="allowed")] string, (
#[id="block"] string |
struct Strippable {
stripped_block: #[id="block"] string,
} |
)>
dispatch minecraft:resource[neoforge:data_map_vibration_frequencies] to DataMap<#[id(registry="game_event",tags="allowed")] string, (
int @ 1..15 |
struct VibrationFrequency {
frequency: int @ 1..15,
} |
)>
dispatch minecraft:resource[neoforge:data_map_villager_types] to DataMap<#[id(registry="worldgen/biome",tags="allowed")] string, (
#[id="villager_type"] string |
struct BiomeVillagerType {
villager_type: #[id="villager_type"] string,
} |
)>
dispatch minecraft:resource[neoforge:data_map_waxables] to DataMap<#[id(registry="block",tags="allowed")] string, (
#[id="block"] string |
struct Waxable {
waxed: #[id="block"] string,
} |
)>

View File

@@ -0,0 +1,34 @@
use ::java::data::worldgen::feature::block_predicate::BlockPredicate
use ::java::data::worldgen::feature::block_state_provider::BlockStateProvider
use ::java::data::worldgen::feature::tree::TreeDecorator
use ::java::data::worldgen::IntProvider
dispatch minecraft:resource[ohthetreesyoullgrow:configured_feature] to struct ConfiguredFeature {
type: #[id] FeatureTypes,
config: ohthetreesyoullgrow:feature_config[[type]],
}
enum(string) FeatureTypes {
TreeFromNbt = "ohthetreesyoullgrow:tree_from_nbt_v1",
}
dispatch ohthetreesyoullgrow:feature_config[ohthetreesyoullgrow:tree_from_nbt_v1] to struct TreeFromNbt {
/// The path to the trunk structure piece.
base_location: #[id="structure"] string,
/// The path to the canopy structure piece.
canopy_location: #[id="structure"] string,
/// Block filter for which this tree is allowed to grow on. Checks all of the red wool positions defined by the trunk.
can_grow_on_filter: BlockPredicate,
/// Block filter for which this tree's leaves are allowed to place.
can_leaves_place_filter: BlockPredicate,
decorators?: [TreeDecorator],
/// Int provider defining the height of the tree.
height: IntProvider<int>,
leaves_provider: BlockStateProvider,
leaves_target: [#[id="block"] string],
log_provider: BlockStateProvider,
log_target: [#[id="block"] string],
max_log_depth?: int,
/// Additional blocks from the structure pieces that should be placed in the world.
place_from_nbt: [#[id="block"] string],
}

1395
public/mcdoc/pixelmon.mcdoc Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
dispatch minecraft:resource[sky_aesthetics:sky] to struct SkyProperties {
world: #[id="dimension"] string,
id?: string,
cloud_settings: CloudSettings,
fog_settings?: FogSettings,
rain: boolean,
custom_vanilla_objects: CustomVanillaObjects,
stars: Star,
/// The R, G and B value for the color
sunrise_color?: [float] @ 3,
sunrise_alpha_modifier?: float,
sky_type: SkyType,
sky_color: struct {
custom_color: boolean,
/// The R, G, B and alpha value for the color
#[until="1.21.2"]
color: [float] @ 4,
/// The R, G and B value for the color
#[since="1.21.3"]
color: [float] @ 3 ,
},
sky_objects: [SkyObject],
constellations: [string],
condition: RenderCondition
}
struct CloudSettings {
cloud: boolean,
cloud_height: int,
/// The R, G and B value for the color
cloud_color?: struct CustomCloudColor {
base_color: [double] @ 3,
storm_color: [double] @ 3,
rain_color: [double] @ 3,
always_base_color: boolean
}
}
struct FogSettings {
fog: boolean,
/// The R, G, B and alpha value for the color
fog_color: [float] @ 4,
fog_density: [float] @ 2,
}
struct CustomVanillaObjects {
sun: boolean,
sun_texture: string,
sun_height: int,
sun_size: int,
moon: boolean,
moon_phase: boolean,
moon_texture: string,
moon_height: int,
moon_size: int,
}
struct Star {
vanilla: boolean,
moving_stars: boolean,
count: int,
all_days_visible: boolean,
scale: float,
/// The R, G and B value for the color
color: [float] @ 3,
shooting_stars? : struct shootingStars {
percentage: int,
random_lifetime: [double] @ 2,
scale: float,
speed: float,
color: [double] @ 3,
rotation?: int
}
}
struct SkyObject {
texture: string,
blend: boolean,
size: float,
height: int,
rotation: [float] @ 3,
rotation_type: RotationType
}
struct RenderCondition {
condition: boolean,
biome?: #[id="worldgen/biome"] string,
biomes?: #[id(registry="worldgen/biome",tags=allowed)] string,
}
enum(string) SkyType {
#[starred] Overworld = "OVERWORLD",
None = "NONE",
End = "END"
}
enum(string) RotationType {
Day = "DAY",
Night = "NIGHT",
Fixed = "FIXED"
}

235
public/mcdoc/thermoo.mcdoc Normal file
View File

@@ -0,0 +1,235 @@
use ::java::data::worldgen::biome::Precipitation
use ::java::util::attribute::AttributeOperation
use ::java::data::util::MinMaxBounds
dispatch minecraft:resource[thermoo:environment_provider] to struct EnvironmentProvider {
type: #[id] EnvironmentProviderType,
...thermoo:environment_provider_type[[type]],
}
type BiomeHolderList = (#[id(registry="worldgen/biome",tags="allowed")] string | [#[id="worldgen/biome"] string])
dispatch minecraft:resource[thermoo:environment] to struct Environment {
biomes: BiomeHolderList,
exclude_biomes?: BiomeHolderList,
provider: EnvironmentProviderOrReference,
/// Defaults to `1000`. Higher priority environments will be applied first, lower priority environments are applied last.
priority?: int
}
dispatch minecraft:resource[thermoo:temperature_effect] to struct TemperatureEffect {
type: #[id] TemperatureEffectType,
config: struct {
...thermoo:temperature_effect_type[[%parent.type]]
}
}
dispatch minecraft:resource[thermoo:predicate] to struct ThermooPredicate {
condition: #[id] ThermooLootConditionType,
...thermoo:loot_condition_type[[condition]]
}
type EnvironmentProviderOrReference = (
#[id="thermoo:environment_provider"] string |
EnvironmentProvider
)
enum(string) EnvironmentProviderType {
Constant = "thermoo:constant",
TemperateSeasonal = "thermoo:seasonal/temperate",
TropicalSeasonal = "thermoo:seasonal/tropical",
LightThreshold = "thermoo:light_threshold",
WeatherState = "thermoo:weather_state",
PrecipitationType = "thermoo:precipitation_type",
TemperatureShift = "thermoo:temperature_shift",
Modify = "thermoo:modify",
}
enum(string) TemperatureEffectType {
Empty = "thermoo:empty",
StatusEffect = "thermoo:status_effect",
AttributeModifier = "thermoo:attribute_modifier",
ScalingAttributeModifier = "thermoo:scaling_attribute_modifier",
Damage = "thermoo:damage",
Function = "thermoo:function",
Sequence = "thermoo:sequence",
FreezeDamageLegacy = "thermoo:freeze_damage_legacy",
}
enum(string) ThermooLootConditionType {
Temperature = "thermoo:temperature",
Soaked = "thermoo:soaked",
}
enum(string) EnvironmentComponentType {
Temperature = "thermoo:temperature",
RelativeHumidity = "thermoo:relative_humidity",
}
struct EnvironmentComponentMap {
[EnvironmentComponentType]: thermoo:environment_component[[%key]],
}
enum(string) TemperatureUnit {
Celsius = "celsius",
Kelvin = "kelvin",
Fahrenheit = "fahrenheit",
Rankine = "rankine",
}
type TemperatureRecord = (
double |
struct {
value: double,
unit: TemperatureUnit,
}
)
dispatch thermoo:environment_component[thermoo:temperature] to TemperatureRecord
dispatch thermoo:environment_component[thermoo:relative_humidity] to double @ 0..1
dispatch thermoo:environment_provider_type[thermoo:constant] to struct {
components: EnvironmentComponentMap,
}
enum(string) TemperateSeason {
Spring = "spring",
Summer = "summer",
Autumn = "autumn",
Winter = "winter",
}
enum(string) TropicalSeason {
Wet = "wet",
Dry = "dry",
}
dispatch thermoo:environment_provider_type[thermoo:seasonal/temperate] to struct {
/// Must contain at least one entry.
seasons: struct {
[TemperateSeason]: EnvironmentProviderOrReference
},
/// If specified, the `fallback_season` must be a member of the `seasons` field.
fallback_season?: TemperateSeason,
}
dispatch thermoo:environment_provider_type[thermoo:seasonal/tropical] to struct {
/// Must contain at least one entry.
seasons: struct {
[TropicalSeason]: EnvironmentProviderOrReference
},
/// If specified, the `fallback_season` must be a member of the `seasons` field.
fallback_season?: TropicalSeason,
}
dispatch thermoo:environment_provider_type[thermoo:light_threshold] to struct {
light_type?: ("block" | "sky"),
/// Only applies if `light_type` is `sky`.
apply_ambient_darkness?: boolean,
threshold: int @ 0..15,
above: EnvironmentProviderOrReference,
below: EnvironmentProviderOrReference,
}
dispatch thermoo:environment_provider_type[thermoo:weather_state] to struct {
clear?: EnvironmentProviderOrReference,
rain?: EnvironmentProviderOrReference,
thunder?: EnvironmentProviderOrReference,
}
dispatch thermoo:environment_provider_type[thermoo:precipitation_type] to struct {
precipitation_type: struct {
[Precipitation]?: EnvironmentProviderOrReference,
}
}
dispatch thermoo:environment_provider_type[thermoo:temperature_shift] to struct {
shift: TemperatureRecord
}
dispatch thermoo:environment_provider_type[thermoo:modify] to struct {
modifiers: (
[#[id(registry="thermoo:environment_provider")] string] @ 1.. |
#[id(registry="thermoo:environment_provider",tags="allowed")] string |
)
}
dispatch thermoo:temperature_effect_type[thermoo:empty] to struct {
}
struct StatusEffectEntry {
type: #[id="mob_effect"] string,
duration?: int @ 1..,
amplifier: int @ 0..
}
dispatch thermoo:temperature_effect_type[thermoo:status_effect] to struct {
effects: [StatusEffectEntry]
}
#[since="1.21"]
dispatch thermoo:temperature_effect_type[thermoo:attribute_modifier] to struct {
value: float,
attribute_type: #[id="attribute"] string,
/// Used when equipping and unequipping the item to identify which modifier to add or remove from the entity.
id: #[id="attribute_modifier"] string,
operation: AttributeOperation
}
dispatch thermoo:temperature_effect_type[thermoo:scaling_attribute_modifier] to struct {
/// Default value of `1.0`.
scale?: float,
attribute_type: #[id="attribute"] string,
/// Used when equipping and unequipping the item to identify which modifier to add or remove from the entity.
#[since="1.21"]
id: #[id="attribute_modifier"] string,
operation: (
#[until="1.20.5"] ("addition" | "multiply_base" | "multiply_total") |
#[since="1.20.5"] AttributeOperation |
),
#[until="1.21"]
name: string,
#[until="1.21"]
modifier_uuid: #[uuid] string
}
dispatch thermoo:temperature_effect_type[thermoo:damage] to struct {
amount: float @ 0<..,
damage_interval: int @ 1..,
damage_type: #[id="damage_type"] string,
}
#[until="1.20.2"]
dispatch thermoo:temperature_effect_type[thermoo:freeze_damage_legacy] to struct {
amount: float @ 0<..,
damage_interval: int @ 1..,
}
#[since="1.21"]
dispatch thermoo:temperature_effect_type[thermoo:function] to struct {
function: #[id="function"] string,
/// Interpreted as an SNBT string. Required if and only if the specified `function` is a macro function.
arguments?: string,
/// Defaults to `20`.
interval?: int @ 1..,
/// Defaults to `2`.
permission_level?: int @ 0..4,
}
#[since="1.21"]
dispatch thermoo:temperature_effect_type[thermoo:sequence] to struct {
children: [TemperatureEffect]
}
dispatch thermoo:loot_condition_type[thermoo:temperature] to struct {
value?: MinMaxBounds<int>,
scale?: MinMaxBounds<double>,
}
dispatch thermoo:loot_condition_type[thermoo:soaked] to struct {
value?: MinMaxBounds<int>,
scale?: MinMaxBounds<double>,
}

12
spyglass.json Normal file
View File

@@ -0,0 +1,12 @@
{
"env": {
"dependencies": [
"@vanilla-mcdoc"
],
"exclude": [
".*/**",
"**/node_modules/**",
"**/dist/**"
]
}
}

View File

@@ -1,47 +1,17 @@
import type { ColormapType } from './components/previews/Colormap.js'
import type { VersionId } from './services/index.js'
type Method = 'menu' | 'hotkey'
export type Method = 'menu' | 'hotkey'
export namespace Analytics {
/** Universal Analytics */
const ID_SITE = 'Site'
const ID_GENERATOR = 'Generator'
const DIM_THEME = 1
const DIM_VERSION = 3
const DIM_LANGUAGE = 4
const DIM_GENERATOR = 6
const DIM_PREFERS_COLOR_SCHEME = 7
function event(category: string, action: string, label?: string) {
ga('send', 'event', category, action, label)
export function pageview(url: string) {
gtag('event', 'page_view', {
page_location: url,
page_title: document.title,
})
}
function dimension(index: number, value: string) {
ga('set', `dimension${index}`, value)
}
export function pageview(page: string) {
ga('set', 'page', page)
ga('send', 'pageview')
}
/**
* @deprecated
*/
export function generatorEvent(action: string, label?: string) {
event(ID_GENERATOR, action, label)
}
function legacyMethod(method: Method) {
return method === 'menu' ? 'Menu' : 'Hotkey'
}
/** END Universal Analytics 4 */
export function setLocale(locale: string) {
dimension(DIM_LANGUAGE, locale)
gtag('set', {
locale,
})
@@ -49,14 +19,12 @@ export namespace Analytics {
export function changeLocale(prev_locale: string, locale: string) {
setLocale(locale)
event(ID_SITE, 'set-language', locale)
gtag('event', 'change_locale', {
prev_locale,
})
}
export function setTheme(theme: string) {
dimension(DIM_THEME, theme)
gtag('set', {
theme,
})
@@ -64,14 +32,12 @@ export namespace Analytics {
export function changeTheme(prev_theme: string, theme: string) {
setTheme(theme)
event(ID_SITE, 'set-theme', theme)
gtag('event', 'change_theme', {
prev_theme,
})
}
export function setVersion(version: string) {
dimension(DIM_VERSION, version)
gtag('set', {
version,
})
@@ -85,21 +51,18 @@ export namespace Analytics {
export function changeVersion(prev_version: string, version: string) {
setVersion(version)
event(ID_GENERATOR, 'set-version', version)
gtag('event', 'change_version', {
prev_version,
})
}
export function setGenerator(file_type: string) {
dimension(DIM_GENERATOR, file_type)
gtag('event', 'use_generator', {
file_type,
})
}
export function setPrefersColorScheme(prefers_color_scheme: string) {
dimension(DIM_PREFERS_COLOR_SCHEME, prefers_color_scheme)
gtag('set', {
prefers_color_scheme,
})
@@ -118,7 +81,6 @@ export namespace Analytics {
}
export function resetGenerator(file_type: string, history: number, method: Method) {
event(ID_GENERATOR, 'reset')
gtag('event', 'reset_generator', {
file_type,
history,
@@ -127,7 +89,6 @@ export namespace Analytics {
}
export function undoGenerator(file_type: string, history: number, method: Method) {
event(ID_GENERATOR, 'undo', legacyMethod(method))
gtag('event', 'undo_generator', {
file_type,
history,
@@ -136,7 +97,6 @@ export namespace Analytics {
}
export function redoGenerator(file_type: string, history: number, method: Method) {
event(ID_GENERATOR, 'redo', legacyMethod(method))
gtag('event', 'redo_generator', {
file_type,
history,
@@ -145,7 +105,6 @@ export namespace Analytics {
}
export function loadPreset(file_type: string, file_name: string) {
event(ID_GENERATOR, 'load-preset', file_name)
gtag('event', 'load_generator_preset', {
file_type,
file_name,
@@ -179,7 +138,6 @@ export namespace Analytics {
}
export function copyOutput(file_type: string, method: Method) {
event(ID_GENERATOR, 'copy')
gtag('event', 'copy_generator_output', {
file_type,
method,
@@ -187,7 +145,6 @@ export namespace Analytics {
}
export function downloadOutput(file_type: string, method: Method) {
event(ID_GENERATOR, 'download')
gtag('event', 'download_generator_output', {
file_type,
method,
@@ -195,7 +152,6 @@ export namespace Analytics {
}
export function showOutput(file_type: string, method: Method) {
event(ID_GENERATOR, 'toggle-output', 'visible')
gtag('event', 'show_generator_output', {
file_type,
method,
@@ -203,7 +159,6 @@ export namespace Analytics {
}
export function hideOutput(file_type: string, method: Method) {
event(ID_GENERATOR, 'toggle-output', 'hidden')
gtag('event', 'hide_generator_output', {
file_type,
method,
@@ -211,7 +166,6 @@ export namespace Analytics {
}
export function showPreview(file_type: string, method: Method) {
event(ID_GENERATOR, 'toggle-preview', 'visible')
gtag('event', 'show_generator_preview', {
file_type,
method,
@@ -219,68 +173,44 @@ export namespace Analytics {
}
export function hidePreview(file_type: string, method: Method) {
event(ID_GENERATOR, 'toggle-preview', 'hidden')
gtag('event', 'hide_generator_preview', {
file_type,
method,
})
}
export function showProject(file_type: string, projects_count: number, project_size: number, method: Method) {
event(ID_GENERATOR, 'show-project', legacyMethod(method))
export function showProject(method: Method) {
gtag('event', 'show_project', {
file_type,
projects_count,
project_size,
method,
})
}
export function hideProject(file_type: string, projects_count: number, project_size: number, method: Method) {
event(ID_GENERATOR, 'hide-project', legacyMethod(method))
export function hideProject(method: Method) {
gtag('event', 'hide_project', {
file_type,
projects_count,
project_size,
method,
})
}
export function saveProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
event(ID_GENERATOR, 'save-project-file', legacyMethod(method))
export function saveProjectFile(method: Method) {
gtag('event', 'save_project_file', {
file_type,
projects_count,
project_size,
method,
})
}
export function deleteProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
event(ID_GENERATOR, 'delete-project-file', legacyMethod(method))
export function deleteProjectFile(method: Method) {
gtag('event', 'delete_project_file', {
file_type,
projects_count,
project_size,
method,
})
}
export function renameProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
event(ID_GENERATOR, 'rename-project-file', legacyMethod(method))
export function renameProjectFile(method: Method) {
gtag('event', 'rename_project_file', {
file_type,
projects_count,
project_size,
method,
})
}
export function deleteProject(projects_count: number, project_size: number, method: Method) {
event(ID_GENERATOR, 'delete-project', legacyMethod(method))
export function deleteProject(method: Method) {
gtag('event', 'delete_project', {
projects_count,
project_size,
method,
})
}

View File

@@ -3,9 +3,9 @@ import { Router } from 'preact-router'
import '../styles/global.css'
import '../styles/nodes.css'
import { Analytics } from './Analytics.js'
import { cleanUrl } from './Utils.js'
import { Header } from './components/index.js'
import { Changelog, Customized, Generator, Generators, Guide, Guides, Home, Partners, Sounds, Transformation, Versions, WhatsNew, Worldgen } from './pages/index.js'
import { Changelog, Convert, Customized, Generator, Generators, Guide, Guides, Home, LegacyPartners, Partners, Sounds, Transformation, Versions, WhatsNew, Worldgen } from './pages/index.js'
import { cleanUrl } from './Utils.js'
export function App() {
const changeRoute = (e: RouterOnChangeArgs) => {
@@ -21,11 +21,14 @@ export function App() {
<Generators path="/generators" />
<Worldgen path="/worldgen" />
<Partners path="/partners" />
<LegacyPartners path="/partners/:id" />
<Sounds path="/sounds" />
<Changelog path="/changelog" />
<Versions path="/versions" />
<Transformation path="/transformation" />
<Customized path="/customized" />
<Convert path="/convert" />
<Convert path="/convert/:formats" />
<WhatsNew path="/whats-new" />
<Guides path="/guides" />
<Guide path="/guides/:id" />

View File

@@ -1,10 +1,10 @@
import config from '../config.json'
import type { VersionId } from './services/Schemas.js'
import type { VersionId } from './services/Versions.js'
export interface ConfigLanguage {
code: string,
name: string,
schemas?: boolean,
mc: string,
}
export interface ConfigVersion {
@@ -19,11 +19,12 @@ export interface ConfigVersion {
export interface ConfigGenerator {
id: string,
url: string,
schema: string,
path?: string,
ext?: string,
noPath?: boolean,
tags?: string[],
partner?: string,
aliases?: string[],
dependency?: string,
minVersion?: string,
maxVersion?: string,
wiki?: string,

View File

@@ -4,6 +4,8 @@ import '../styles/main.css'
import '../styles/nodes.css'
import { App } from './App.js'
import { LocaleProvider, ProjectProvider, StoreProvider, ThemeProvider, TitleProvider, VersionProvider } from './contexts/index.js'
import { ModalProvider } from './contexts/Modal.jsx'
import { SpyglassProvider } from './contexts/Spyglass.jsx'
function Main() {
return (
@@ -12,9 +14,13 @@ function Main() {
<ThemeProvider>
<VersionProvider>
<TitleProvider>
<ProjectProvider>
<App />
</ProjectProvider>
<SpyglassProvider>
<ProjectProvider>
<ModalProvider>
<App />
</ModalProvider>
</ProjectProvider>
</SpyglassProvider>
</TitleProvider>
</VersionProvider>
</ThemeProvider>

View File

@@ -1,9 +1,10 @@
import type { ColormapType } from './components/previews/Colormap.js'
import { ColormapTypes } from './components/previews/Colormap.js'
import type { Project } from './contexts/index.js'
import type { ProjectMeta } from './contexts/index.js'
import { DRAFT_PROJECT } from './contexts/index.js'
import type { VersionId } from './services/index.js'
import { DEFAULT_VERSION, VersionIds } from './services/index.js'
import { safeJsonParse } from './Utils.js'
export namespace Store {
export const ID_LANGUAGE = 'language'
@@ -14,7 +15,6 @@ export namespace Store {
export const ID_HIGHLIGHTING = 'output_highlighting'
export const ID_SOUNDS_VERSION = 'minecraft_sounds_version'
export const ID_PROJECTS = 'misode_projects'
export const ID_BACKUPS = 'misode_generator_backups'
export const ID_PREVIEW_PANEL_OPEN = 'misode_preview_panel_open'
export const ID_PROJECT_PANEL_OPEN = 'misode_project_panel_open'
export const ID_OPEN_PROJECT = 'misode_open_project'
@@ -63,29 +63,24 @@ export namespace Store {
return localStorage.getItem(ID_SOUNDS_VERSION) ?? 'latest'
}
export function getProjects(): Project[] {
export function getProjects(): ProjectMeta[] {
const projects = localStorage.getItem(ID_PROJECTS)
if (projects) {
return JSON.parse(projects) as Project[]
return safeJsonParse(projects) ?? []
}
return [DRAFT_PROJECT]
}
export function getBackup(id: string): object | undefined {
const backups = JSON.parse(localStorage.getItem(ID_BACKUPS) ?? '{}')
return backups[id]
}
export function getPreviewPanelOpen(): boolean | undefined {
const open = localStorage.getItem(ID_PREVIEW_PANEL_OPEN)
if (open === null) return undefined
return JSON.parse(open)
return safeJsonParse(open)
}
export function getProjectPanelOpen(): boolean | undefined {
const open = localStorage.getItem(ID_PROJECT_PANEL_OPEN)
if (open === null) return undefined
return JSON.parse(open)
return safeJsonParse(open)
}
export function getOpenProject() {
@@ -105,7 +100,8 @@ export namespace Store {
}
export function getGeneratorHistory(): string[] {
return JSON.parse(localStorage.getItem(ID_GENERATOR_HISTORY) ?? '[]')
const value = localStorage.getItem(ID_GENERATOR_HISTORY) ?? '[]'
return safeJsonParse(value) ?? []
}
export function setLanguage(language: string | undefined) {
@@ -136,20 +132,10 @@ export namespace Store {
if (version) localStorage.setItem(ID_SOUNDS_VERSION, version)
}
export function setProjects(projects: Project[] | undefined) {
export function setProjects(projects: ProjectMeta[] | undefined) {
if (projects) localStorage.setItem(ID_PROJECTS, JSON.stringify(projects))
}
export function setBackup(id: string, data: object | undefined) {
const backups = JSON.parse(localStorage.getItem(ID_BACKUPS) ?? '{}')
if (data === undefined) {
delete backups[id]
} else {
backups[id] = data
}
localStorage.setItem(ID_BACKUPS, JSON.stringify(backups))
}
export function setPreviewPanelOpen(open: boolean | undefined) {
if (open === undefined) {
localStorage.removeItem(ID_PREVIEW_PANEL_OPEN)
@@ -189,7 +175,8 @@ export namespace Store {
}
export function getWhatsNewSeen(): { id: string, time: string }[] {
return JSON.parse(localStorage.getItem(ID_WHATS_NEW_SEEN) ?? '[]')
const value = localStorage.getItem(ID_WHATS_NEW_SEEN) ?? '[]'
return safeJsonParse(value) ?? []
}
export function seeWhatsNew(ids: string[]) {

View File

@@ -1,14 +1,17 @@
import type { DataModel } from '@mcschema/core'
import { Path } from '@mcschema/core'
import * as zip from '@zip.js/zip.js'
import type { Random } from 'deepslate'
import { Matrix3, Matrix4, Vector } from 'deepslate'
import type { Identifier, NbtTag, Random } from 'deepslate'
import { Matrix3, Matrix4, NbtByte, NbtCompound, NbtDouble, NbtInt, NbtList, NbtString, Vector } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { quat, vec2 } from 'gl-matrix'
import yaml from 'js-yaml'
import { route } from 'preact-router'
import rfdc from 'rfdc'
import type { ConfigGenerator } from './Config.js'
import config from './Config.js'
import type { VersionId } from './services/index.js'
import { checkVersion } from './services/index.js'
export const SOURCE_REPO_URL = 'https://github.com/misode/misode.github.io'
export function isPromise(obj: any): obj is Promise<any> {
return typeof (obj as any)?.then === 'function'
@@ -29,7 +32,11 @@ export function hexId(length = 12) {
}
export function randomSeed() {
return BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
return BigInt(Math.floor((Math.random() - 0.5) * 2 * Number.MAX_SAFE_INTEGER))
}
export function randomInt() {
return Math.floor(Math.random() * 4294967296) - 2147483648
}
export function generateUUID() {
@@ -44,19 +51,17 @@ export function generateColor() {
return Math.floor(Math.random() * 16777215)
}
export function newSeed(model: DataModel) {
const seed = Math.floor(Math.random() * (4294967296)) - 2147483648
const dimensions = model.get(new Path(['dimensions']))
model.set(new Path(['seed']), seed, true)
if (isObject(dimensions)) {
Object.keys(dimensions).forEach(id => {
model.set(new Path(['dimensions', id, 'generator', 'seed']), seed, true)
model.set(new Path(['dimensions', id, 'generator', 'biome_source', 'seed']), seed, true)
})
}
model.set(new Path(['placement', 'salt']), Math.abs(seed), true)
model.set(new Path(['generator', 'seed']), seed, true)
model.set(new Path(['generator', 'biome_source', 'seed']), seed)
function intToUnsigned(n: number) {
n |= 0 // Force to signed 32-bit integer
return n < 0 ? n + 0x100000000 : n
}
export function intToHexRgb(c: number | undefined) {
return c ? '#' + (c & 0xFFFFFF).toString(16).padStart(6, '0') : '#000000'
}
export function intToDisplayHexRgb(c: number | undefined) {
return c ? '#' + intToUnsigned(c).toString(16).toUpperCase().padStart(6, '0') : '#000000'
}
export function htmlEncode(str: string) {
@@ -72,7 +77,7 @@ export function hashString(s: string) {
}
export function cleanUrl(url: string) {
return `/${url}/`.replaceAll('//', '/')
return `/${url}/`.replaceAll(/\/\/+/g, '/')
}
export function getPath(url: string) {
@@ -305,23 +310,23 @@ export class BiMap<A, B> {
}
}
export async function readZip(file: File | ArrayBuffer, predicate: (name: string) => boolean = () => true): Promise<[string, string][]> {
export async function readZip(file: File | ArrayBuffer, predicate: (name: string) => boolean = () => true): Promise<[string, Uint8Array][]> {
const buffer = file instanceof File ? await file.arrayBuffer() : file
const reader = new zip.ZipReader(new zip.BlobReader(new Blob([buffer])))
const entries = await reader.getEntries()
return await Promise.all(entries
.filter(e => !e.directory && predicate(e.filename))
.map(async e => {
const writer = new zip.TextWriter('utf-8')
return [e.filename, await e.getData?.(writer)] as [string, string]
const writer = new zip.Uint8ArrayWriter()
return [e.filename, await e.getData?.(writer)]
})
)
}
export async function writeZip(entries: [string, string][]): Promise<string> {
export async function writeZip(entries: [string, Uint8Array][]): Promise<string> {
const writer = new zip.ZipWriter(new zip.Data64URIWriter('application/zip'))
await Promise.all(entries.map(async ([name, data]) => {
await writer.add(name, new zip.TextReader(data))
await writer.add(name, new zip.Uint8ArrayReader(data))
}))
return await writer.close()
}
@@ -559,6 +564,11 @@ export function parseGitPatch(patch: string) {
let after = 1
for (let i = 0; i < source.length; i += 1) {
const line = source[i]
if (line.startsWith('Index: ') || line.startsWith('===')
|| line.startsWith('---') || line.startsWith('+++')
|| line.startsWith('\\') || line.length === 0) {
continue
}
if (line.startsWith('@')) {
const match = line.match(/^@@ -(\d+)(?:,(?:\d+))? \+(\d+)(?:,(?:\d+))? @@/)
if (!match) throw new Error(`Invalid patch pattern at line ${i+1}: ${line}`)
@@ -575,9 +585,68 @@ export function parseGitPatch(patch: string) {
} else if (line.startsWith('-')) {
result.push({ line, before })
before += 1
} else if (!line.startsWith('\\')) {
throw new Error(`Invalid patch, got ${line.charAt(0)} at line ${i+1}`)
} else {
throw new Error(`Invalid patch, got '${line.charAt(0)}' at line ${i+1}`)
}
}
return result
}
const legacyFolders = new Set(['loot_table', 'predicate', 'item_modifier', 'advancement', 'recipe', 'tag/function', 'tag/item', 'tag/block', 'tag/fluid', 'tag/entity_type', 'tag/game_event'])
export function genPath(gen: ConfigGenerator, version: VersionId) {
const path = gen.path ?? gen.id
if (!checkVersion(version, '1.21') && legacyFolders.has(gen.id)) {
return path + 's'
}
return path
}
export function jsonToNbt(value: unknown): NbtTag {
if (typeof value === 'string') {
return new NbtString(value)
}
if (typeof value === 'number') {
return Number.isInteger(value) ? new NbtInt(value) : new NbtDouble(value)
}
if (typeof value === 'boolean') {
return new NbtByte(value)
}
if (Array.isArray(value)) {
return new NbtList(value.map(jsonToNbt))
}
if (typeof value === 'object' && value !== null) {
return new NbtCompound(
new Map(Object.entries(value ?? {})
.map(([k, v]) => [k, jsonToNbt(v)]))
)
}
return new NbtByte(0)
}
export function mergeTextComponentStyles(text: unknown, style: Record<string, unknown>) {
if (typeof text === 'string') {
return { ...style, text }
}
if (Array.isArray(text)) {
return { ...style, ...text[0], extra: text.slice(1) }
}
if (typeof text === 'object' && text !== null) {
return { ...style, ...text }
}
return { ...style, text: '' }
}
export function makeDescriptionId(prefix: string, id: Identifier | undefined) {
if (id === undefined) {
return `${prefix}.unregistered_sadface`
}
return `${prefix}.${id.namespace}.${id.path.replaceAll('/', '.')}`
}
export function safeJsonParse(text: string): any {
try {
return JSON.parse(text)
} catch (e) {
return undefined
}
}

View File

@@ -1,10 +1,12 @@
import type { ComponentChildren } from 'preact'
import { getCurrentUrl } from 'preact-router'
import { useEffect, useMemo, useState } from 'preact/hooks'
import { Store } from '../Store.js'
import { getGenerator } from '../Utils.js'
import { useProject } from '../contexts/Project.jsx'
import { useSpyglass } from '../contexts/Spyglass.jsx'
import { useVersion } from '../contexts/Version.jsx'
import { useAsync } from '../hooks/useAsync.js'
import { latestVersion } from '../services/DataFetcher.js'
import { getGenerator, SOURCE_REPO_URL } from '../Utils.js'
import { Octicon } from './index.js'
type ErrorPanelProps = {
@@ -17,12 +19,23 @@ type ErrorPanelProps = {
}
export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_, children }: ErrorPanelProps) {
const { version } = useVersion()
const { service } = useSpyglass()
const { projectUri } = useProject()
const [stackVisible, setStackVisible] = useState(false)
const [stack, setStack] = useState<string | undefined>(undefined)
const gen = getGenerator(getCurrentUrl())
const source = gen ? Store.getBackup(gen.id) : undefined
const name = (prefix ?? '') + (error instanceof Error ? error.message : error)
const gen = getGenerator(getCurrentUrl())
const { value: source } = useAsync(async () => {
if (!service || !gen) {
return undefined
}
const uri = projectUri ?? service.getUnsavedFileUri(gen)
if (!uri) {
return undefined
}
return await service.readFile(uri)
}, [service, version, projectUri, gen])
useEffect(() => {
if (error instanceof Error) {
@@ -42,7 +55,7 @@ export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_,
}, [error])
const url = useMemo(() => {
let url ='https://github.com/misode/misode.github.io/issues/new'
let url =`${SOURCE_REPO_URL}/issues/new`
const fullName = (error instanceof Error ? `${error.name}: ` : '') + name
url += `?title=${encodeURIComponent(fullName)}`
let body = ''
@@ -56,7 +69,7 @@ export function ErrorPanel({ error, prefix, reportable, onDismiss, body: body_,
body += `\n### Stack trace\n\`\`\`\n${fullName}\n${stack}\n\`\`\`\n`
}
if (source) {
body += `\n### Generator JSON\n<details>\n<pre>\n${JSON.stringify(source, null, 2)}\n</pre>\n</details>\n`
body += `\n### Generator JSON\n<details>\n<pre>\n${source}\n</pre>\n</details>\n`
}
if (body_) {
body += body_

View File

@@ -0,0 +1,67 @@
import type { ComponentChildren } from 'preact'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useFocus } from '../hooks/index.js'
interface Props {
placeholder?: string
relative?: boolean
class?: string
getResults: (search: string, close: () => void) => ComponentChildren
children: ComponentChildren
}
export function FancyMenu({ placeholder, relative, class: clazz, getResults, children }: Props) {
const [active, setActive] = useFocus()
const [search, setSearch] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const resultsRef = useRef<HTMLDivElement>(null)
const results = useMemo(() => {
return getResults(search, () => setActive(false))
}, [getResults, setActive, search])
const open = useCallback(() => {
setActive(true)
setTimeout(() => {
inputRef.current?.select()
})
}, [setActive, inputRef])
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key == 'Enter') {
if (document.activeElement == inputRef.current) {
const firstResult = resultsRef.current?.firstElementChild
if (firstResult instanceof HTMLElement) {
firstResult.click()
}
}
} else if (e.key == 'ArrowDown') {
const nextElement = document.activeElement == inputRef.current
? resultsRef.current?.firstElementChild
: document.activeElement?.nextElementSibling
if (nextElement instanceof HTMLElement) {
nextElement.focus()
}
e.preventDefault()
} else if (e.key == 'ArrowUp') {
const prevElement = document.activeElement?.previousElementSibling
if (prevElement instanceof HTMLElement) {
prevElement.focus()
}
e.preventDefault()
} else if (e.key == 'Escape') {
setActive(false)
}
}, [setActive, inputRef])
return <div class={`${relative ? 'relative' : ''}`}>
<div onClick={open}>
{children}
</div>
<div class={`fancy-menu absolute flex flex-col gap-2 p-2 rounded-lg drop-shadow-xl ${clazz} ${active ? '' : 'hidden'}`} onKeyDown={handleKeyDown}>
<input ref={inputRef} type="text" class="py-1 px-2 w-full rounded" value={search} placeholder={placeholder} onInput={(e) => setSearch((e.target as HTMLInputElement).value)} onClick={(e) => e.stopPropagation()} />
{active && <div ref={resultsRef} class="fancy-menu-results overflow-y-auto overscroll-none flex flex-col pr-2 h-96 max-h-max w-max max-w-full">
{results}
</div>}
</div>
</div>
}

View File

@@ -1,4 +1,5 @@
import { useLocale } from '../contexts/index.js'
import { SOURCE_REPO_URL } from '../Utils.js'
import { Octicon } from './index.js'
interface Props {
@@ -17,7 +18,7 @@ export function Footer({ donate }: Props) {
</p>}
<p>
{Octicon.mark_github}
<span>{locale('source_code_on')} <a href="https://github.com/misode/misode.github.io" target="_blank" rel="noreferrer">{locale('github')}</a></span>
<span>{locale('source_code_on')} <a href={SOURCE_REPO_URL} target="_blank" rel="noreferrer">{locale('github')}</a></span>
</p>
</footer>
}

View File

@@ -1,8 +1,11 @@
import { getCurrentUrl, Link, route } from 'preact-router'
import { getCurrentUrl, Link } from 'preact-router'
import { useCallback } from 'preact/hooks'
import type { ConfigGenerator } from '../Config.js'
import config from '../Config.js'
import { useLocale, useProject, useTheme, useTitle, useVersion } from '../contexts/index.js'
import { checkVersion } from '../services/index.js'
import { cleanUrl, getGenerator } from '../Utils.js'
import { useLocale, useTheme, useTitle, useVersion } from '../contexts/index.js'
import { cleanUrl, getGenerator, SOURCE_REPO_URL } from '../Utils.js'
import { FancyMenu } from './FancyMenu.jsx'
import { searchGenerators } from './generator/GeneratorList.jsx'
import { Btn, BtnMenu, Icons, Octicon } from './index.js'
const Themes: Record<string, keyof typeof Octicon> = {
@@ -14,32 +17,20 @@ const Themes: Record<string, keyof typeof Octicon> = {
export function Header() {
const { lang, locale, changeLocale: changeLanguage } = useLocale()
const { theme, changeTheme } = useTheme()
const { version } = useVersion()
const { projects, project, changeProject } = useProject()
const { title } = useTitle()
const url = getCurrentUrl()
const gen = getGenerator(url)
return <header>
<div class="title">
<Link class="home-link" href="/" aria-label={locale('home')} data-cy="home-link">{Icons.home}</Link>
<h1 class="font-bold">{title}</h1>
{gen && <BtnMenu icon="chevron_down" tooltip={locale('switch_generator')} data-cy="generator-switcher">
{config.generators
.filter(g => g.tags?.[0] === gen?.tags?.[0] && checkVersion(version, g.minVersion))
.map(g =>
<Btn label={locale(g.partner ? `partner.${g.partner}.${g.id}` : g.id)} active={g.id === gen.id} onClick={() => route(cleanUrl(g.url))} />
)}
</BtnMenu>}
{!gen && url.match(/\/?project\/?$/) && <BtnMenu icon="chevron_down" tooltip={locale('switch_project')}>
{projects.map(p =>
<Btn label={p.name} active={p.name === project.name} onClick={() => changeProject(p.name)} />
)}
</BtnMenu>}
<div class="title flex items-center">
<Link class="home-link pr-1" href="/" aria-label={locale('home')}>{Icons.home}</Link>
{gen
? <GeneratorTitle title={title} gen={gen} />
: <h1 class="font-bold px-1 text-lg sm:text-2xl">{title}</h1>}
</div>
<nav>
<ul>
<li data-cy="language-switcher">
<li>
<BtnMenu icon="globe" tooltip={locale('language')}>
{config.languages.map(({ code, name }) =>
<Btn label={name} active={code === lang}
@@ -47,7 +38,7 @@ export function Header() {
)}
</BtnMenu>
</li>
<li data-cy="theme-switcher">
<li>
<BtnMenu icon={Themes[theme]} tooltip={locale('theme')}>
{Object.entries(Themes).map(([th, icon]) =>
<Btn icon={icon} label={locale(`theme.${th}`)} active={th === theme}
@@ -56,7 +47,7 @@ export function Header() {
</BtnMenu>
</li>
<li class="dimmed">
<a href="https://github.com/misode/misode.github.io" target="_blank" rel="noreferrer" class="tooltipped tip-sw" aria-label={locale('github')}>
<a href={SOURCE_REPO_URL} target="_blank" rel="noreferrer" class="tooltipped tip-sw" aria-label={locale('github')}>
{Octicon.mark_github}
</a>
</li>
@@ -64,3 +55,41 @@ export function Header() {
</nav>
</header>
}
interface GeneratorTitleProps {
title: string
gen: ConfigGenerator
}
function GeneratorTitle({ title, gen }: GeneratorTitleProps) {
const { locale } = useLocale()
const { version } = useVersion()
const icon = Object.keys(Icons).includes(gen.id) ? gen.id as keyof typeof Icons : undefined
const getGenerators = useCallback((search: string, close: () => void) => {
let results = config.generators
.filter(g => !g.dependency)
.map(g => ({ ...g, name: locale(`generator.${g.id}`).toLowerCase() }))
results = searchGenerators(results, search)
if (results.length === 0) {
return [<span class="note">{locale('generators.no_results')}</span>]
}
return results.map(g =>
<Link class="flex items-center cursor-pointer no-underline rounded p-1" href={cleanUrl(g.url)} onClick={close}>
{locale(`generator.${g.id}`)}
{Object.keys(Icons).includes(g.id) ? Icons[g.id as keyof typeof Icons] : undefined}
<div class="m-auto"></div>
{g.tags?.filter(t => t === 'assets').map(t =>
<div class="badge ml-2 mr-0 text-sm" style="--color: #555;">{t}</div>
)}
</Link>
)
}, [locale, version])
return <FancyMenu getResults={getGenerators} placeholder={locale('generators.search')}>
<h1 class="font-bold flex items-center cursor-pointer text-lg sm:text-2xl">
{title}
{icon && Icons[icon]}
</h1>
</FancyMenu>
}

View File

@@ -6,20 +6,34 @@ export const Icons = {
report: <svg width="30" height="36" viewBox="0 0 30 36" xmlns="http://www.w3.org/2000/svg"><path d="M0 16C0 13.7909 1.79086 12 4 12V12C6.20914 12 8 13.7909 8 16V32C8 34.2091 6.20914 36 4 36V36C1.79086 36 0 34.2091 0 32V16Z" fill="#6ACC5D"/><path d="M11 4C11 1.79086 12.7909 0 15 0V0C17.2091 0 19 1.79086 19 4V32C19 34.2091 17.2091 36 15 36V36C12.7909 36 11 34.2091 11 32V4Z" fill="#FF4C4C"/><path d="M22 10C22 7.79086 23.7909 6 26 6V6C28.2091 6 30 7.79086 30 10V32C30 34.2091 28.2091 36 26 36V36C23.7909 36 22 34.2091 22 32V10Z" fill="#E5B442"/><path d="M0 23C0 20.7909 1.79086 19 4 19V19C6.20914 19 8 20.7909 8 23V32C8 34.2091 6.20914 36 4 36V36C1.79086 36 0 34.2091 0 32V23Z" fill="#2BAD1D"/><path d="M11 15C11 12.7909 12.7909 11 15 11V11C17.2091 11 19 12.7909 19 15V32C19 34.2091 17.2091 36 15 36V36C12.7909 36 11 34.2091 11 32V15Z" fill="#C10B0B"/><path d="M22 19C22 16.7909 23.7909 15 26 15V15C28.2091 15 30 16.7909 30 19V32C30 34.2091 28.2091 36 26 36V36C23.7909 36 22 34.2091 22 32V19Z" fill="#CC8E00"/></svg>,
sounds: <svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><circle cx="10" cy="10" r="10" fill="#451475"/><path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 10C3.5 8.27609 4.18482 6.62279 5.40381 5.40381C6.62279 4.18482 8.27609 3.5 10 3.5C11.7239 3.5 13.3772 4.18482 14.5962 5.40381C15.8152 6.62279 16.5 8.27609 16.5 10C16.5 11.7239 15.8152 13.3772 14.5962 14.5962C13.3772 15.8152 11.7239 16.5 10 16.5C8.27609 16.5 6.62279 15.8152 5.40381 14.5962C4.18482 13.3772 3.5 11.7239 3.5 10V10ZM10 2C7.87827 2 5.84344 2.84285 4.34315 4.34315C2.84285 5.84344 2 7.87827 2 10C2 12.1217 2.84285 14.1566 4.34315 15.6569C5.84344 17.1571 7.87827 18 10 18C12.1217 18 14.1566 17.1571 15.6569 15.6569C17.1571 14.1566 18 12.1217 18 10C18 7.87827 17.1571 5.84344 15.6569 4.34315C14.1566 2.84285 12.1217 2 10 2V2ZM8.379 7.227C8.34101 7.20412 8.29762 7.19175 8.25327 7.19117C8.20892 7.19059 8.16522 7.20181 8.12664 7.2237C8.08807 7.24558 8.05601 7.27733 8.03375 7.3157C8.0115 7.35406 7.99985 7.39765 8 7.442V12.559C8.00003 12.6033 8.0118 12.6467 8.03413 12.685C8.05646 12.7232 8.08854 12.7548 8.12708 12.7765C8.16563 12.7983 8.20926 12.8095 8.25352 12.8088C8.29778 12.8082 8.34108 12.7958 8.379 12.773L12.643 10.214C12.6798 10.1917 12.7103 10.1604 12.7315 10.1229C12.7526 10.0854 12.7638 10.043 12.7638 10C12.7638 9.95695 12.7526 9.91463 12.7315 9.87714C12.7103 9.83965 12.6798 9.80825 12.643 9.786L8.379 7.227Z" fill="#C5A5E6"/></svg>,
customized: <svg width="28" height="29" viewBox="0 0 28 29" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 16.5V11" stroke="#4BA041" stroke-width="2"/><rect x="15" y="6" width="6" height="6" rx="2" fill="#4BA041"/><path d="M9 11.5V5" stroke="#4BA041" stroke-width="2"/><rect x="6" width="6" height="6" rx="2" fill="#4BA041"/><path d="M24 24H8C5.79086 24 4 22.2091 4 20V8.99999C6 8.5 8 8.49999 9.5 9.99999C10.5 11 11.5 12.9368 13 14.4368C13.9499 15.3867 15.6497 15.9119 17.5 16C19 16.0714 21.078 15.3978 22 14.9368C23 14.4368 26 14 28 14.4368V20C28 22.2091 26.2091 24 24 24Z" fill="#91908F"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6 26.2968H22C22.5869 26.2968 23.1444 26.1704 23.6465 25.9433C23.0189 27.3311 21.6222 28.2968 20 28.2968H4C1.79086 28.2968 0 26.5059 0 24.2968V13.2968C0.673018 13.1285 1.34604 13.0169 2 13V22.2968C2 24.5059 3.79086 26.2968 6 26.2968Z" fill="#4D989B"/></svg>,
convert: <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.55 35.4698C10.9312 35.8188 11.4355 36.0087 11.9565 35.9997C12.4775 35.9907 12.9746 35.7833 13.3431 35.4214C13.7115 35.0595 13.9226 34.5712 13.9318 34.0595C13.941 33.5477 13.7476 33.0525 13.3924 32.678L8.7802 28.1479H32.0823C32.6157 28.1479 33.1272 27.9398 33.5044 27.5693C33.8815 27.1989 34.0934 26.6964 34.0934 26.1725C34.0934 25.6486 33.8815 25.1462 33.5044 24.7757C33.1272 24.4053 32.6157 24.1972 32.0823 24.1972H8.7802L13.3924 19.667C13.7476 19.2926 13.941 18.7973 13.9318 18.2856C13.9226 17.7738 13.7115 17.2855 13.3431 16.9236C12.9746 16.5617 12.4775 16.3544 11.9565 16.3454C11.4355 16.3363 10.9312 16.5263 10.55 16.8752L2.50552 24.7766C2.12891 25.147 1.91737 25.6491 1.91737 26.1725C1.91737 26.696 2.12891 27.1981 2.50552 27.5684L10.55 35.4698Z" fill="#8012C5"/><path d="M25.46 20.5674C25.2758 20.7615 25.0538 20.9171 24.8071 21.0251C24.5604 21.1331 24.294 21.1912 24.024 21.1958C23.7539 21.2005 23.4857 21.1517 23.2352 21.0524C22.9848 20.953 22.7573 20.8051 22.5663 20.6175C22.3753 20.4299 22.2247 20.2065 22.1236 19.9605C22.0224 19.7145 21.9727 19.451 21.9775 19.1857C21.9823 18.9205 22.0414 18.6589 22.1513 18.4166C22.2612 18.1742 22.4197 17.9561 22.6173 17.7753L27.2299 13.2447H3.9256C3.39217 13.2447 2.88058 13.0366 2.50339 12.6661C2.1262 12.2956 1.91429 11.7931 1.91429 11.2692C1.91429 10.7452 2.1262 10.2427 2.50339 9.87225C2.88058 9.50176 3.39217 9.29363 3.9256 9.29363H27.2299L22.6173 4.76306C22.2621 4.38856 22.0686 3.89324 22.0778 3.38144C22.087 2.86964 22.2981 2.38133 22.6666 2.01937C23.0351 1.65742 23.5323 1.45009 24.0533 1.44106C24.5744 1.43203 25.0787 1.622 25.46 1.97096L33.5052 9.87312C33.8819 10.2435 34.0934 10.7456 34.0934 11.2692C34.0934 11.7927 33.8819 12.2948 33.5052 12.6652L25.46 20.5674Z" fill="#E5B442"/>
</svg>,
advancement: <svg width="27" height="27" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.76943 2.86824L2.18356 0.819176C1.29934 0.313911 0.313911 1.29934 0.819176 2.18356L2.86824 5.76943C2.95458 5.92052 3 6.09154 3 6.26556V20.7344C3 20.9085 2.95459 21.0795 2.86824 21.2306L0.819176 24.8164C0.313911 25.7007 1.29934 26.6861 2.18356 26.1808L5.76943 24.1318C5.92052 24.0454 6.09154 24 6.26556 24H20.7344C20.9085 24 21.0795 24.0454 21.2306 24.1318L24.8164 26.1808C25.7007 26.6861 26.6861 25.7007 26.1808 24.8164L24.1318 21.2306C24.0454 21.0795 24 20.9085 24 20.7344V6.26556C24 6.09154 24.0454 5.92052 24.1318 5.76943L26.1808 2.18356C26.6861 1.29934 25.7007 0.313911 24.8164 0.819176L21.2306 2.86824C21.0795 2.95458 20.9085 3 20.7344 3H6.26556C6.09154 3 5.92052 2.95459 5.76943 2.86824Z"/></svg>,
atlas: <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M28 0C30.2091 0 32 1.79086 32 4V28C32 30.2091 30.2091 32 28 32H4L3.79395 31.9951C1.68056 31.8879 0 30.14 0 28V4C0 1.85996 1.68056 0.112115 3.79395 0.00488281L4 0H28ZM3 28C3 28.5523 3.44772 29 4 29H13V26H3V28ZM16 20V29H21V20H16ZM24 29H28C28.5523 29 29 28.5523 29 28V13H24V29ZM3 20V23H13V20H3ZM4 3C3.44772 3 3 3.44772 3 4V17H13V3H4ZM16 17H21V13H16V17ZM16 10H29V4C29 3.44772 28.5523 3 28 3H16V10Z"/></svg>,
banner_pattern: <svg width="17" height="22" viewBox="0 0 17 22" fill="none" xmlns="http://www.w3.org/2000/svg"><rect y="2" width="2" height="17" rx="1" transform="rotate(-90 0 2)"/><path d="M5 19C3.34315 19 2 17.6569 2 16L2 1L15 0.999999L15 16C15 17.6569 13.6569 19 12 19L5 19Z"/><path d="M8 22C6.34315 22 5 20.6569 5 19L5 18L12 18L12 19C12 20.6569 10.6569 22 9 22L8 22Z"/></svg>,
block_definition: <svg width="28" height="30" viewBox="0 0 28 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.75 0.272806C13.5437 -0.0909347 14.4563 -0.0909357 15.25 0.272805L26.25 5.31447C27.3163 5.80322 28 6.86864 28 8.04167V21.3583C28 22.5313 27.3163 23.5967 26.25 24.0855L15.25 29.1272C14.4563 29.4909 13.5437 29.4909 12.75 29.1272L1.75004 24.0855C0.683681 23.5967 0 22.5313 0 21.3583V8.04167C0 6.86864 0.683678 5.80322 1.75004 5.31447L12.75 0.272806ZM14 4.10003L6.92266 7.34381L14 10.2391L21.0773 7.34381L14 4.10003ZM24 10.4699V20.7166L16 24.3833V13.7427L24 10.4699ZM12 13.7427L4 10.4699V20.7166L12 24.3833V13.7427Z"/></svg>,
chat_type: <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 0C1.79086 0 0 1.79086 0 4V21C0 23.2091 1.79086 25 4 25H8V30.7732C8 31.636 9.01946 32.0938 9.66436 31.5206L17 25H28C30.2091 25 32 23.2091 32 21V4C32 1.79086 30.2091 0 28 0H4Z"/></svg>,
damage_type: <svg width="30" height="28" viewBox="0 0 30 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 9V10.8076C1 10.9314 1.04595 11.0509 1.12895 11.1428L15 26.5V8C14.5 5 13 1 7.5 1C2 1 1 6 1 9Z"/><path d="M15 26.5V8M15 26.5L1.12895 11.1428C1.04595 11.0509 1 10.9314 1 10.8076V9C1 6 2 1 7.5 1C13 1 14.5 5 15 8M15 26.5L28.8711 11.1428C28.9541 11.0509 29 10.9314 29 10.8076V9C29 6 27.5 0.999999 23 1C18.5 1 15.5 5 15 8" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round"/></svg>,
dialog: <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M24 0C26.2091 0 28 1.79086 28 4V24C28 26.2091 26.2091 28 24 28H4C1.79086 28 0 26.2091 0 24V4C0 1.79086 1.79086 0 4 0H24ZM15.7324 5C14.9626 3.66667 13.0374 3.66667 12.2676 5L3.60742 20C2.83775 21.3333 3.80029 23 5.33984 23H22.6602C24.1997 23 25.1623 21.3333 24.3926 20L15.7324 5Z" fill="currentColor"/><path d="M14 10L14 15" stroke="currentColor" stroke-width="3" stroke-linecap="round"/><path d="M14 19L14 19.0001" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>,
dimension: <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M19.7659 0.445701C18.5582 0.154357 17.2971 0 16 0C12.7056 0 9.64369 0.995672 7.09887 2.70251C7.94418 3.62868 8.45554 4.49063 8.75229 5.30669C9.09264 6.24263 9.125 7.05145 9.125 7.6875C9.125 7.78933 9.12466 7.88242 9.12434 7.96797V7.96834V7.96869V7.96901V7.96902C9.12247 8.47232 9.12158 8.71409 9.19707 8.93677C9.24638 9.08226 9.36226 9.31309 9.94721 9.60557C10.5615 9.91273 10.7586 9.8353 10.7733 9.82952L10.7739 9.82929C10.8476 9.80165 10.9347 9.73088 11.2191 9.3753L11.256 9.32901C11.4803 9.04661 11.8843 8.53789 12.5239 8.29804C13.2651 8.02009 14.0719 8.16789 14.9472 8.60557C16.4469 9.35541 17.3707 9.72195 17.9896 9.84301C18.2833 9.90046 18.4612 9.89221 18.5724 9.86953C18.6731 9.84897 18.7646 9.80771 18.8753 9.71913C19.2786 9.39647 19.6673 8.76901 19.9604 7.83984C20.2468 6.93156 20.4085 5.83955 20.4378 4.72369C20.4672 3.60868 20.3634 2.50945 20.1445 1.59133C20.0358 1.13577 19.9057 0.75371 19.7659 0.445701ZM5.50368 3.92379C2.13164 6.85717 4.76837e-07 11.1797 4.76837e-07 16C4.76837e-07 16.1436 0.00189157 16.2867 0.00565022 16.4294C1.54401 16.9679 2.98111 16.6462 4.47925 16.2249C4.59499 16.1923 4.71296 16.1585 4.83279 16.1242L4.83282 16.1242C5.68141 15.881 6.62293 15.6113 7.5179 15.5713C8.62706 15.5218 9.72807 15.8138 10.7071 16.7929C11.7646 17.8503 12.0777 19.3283 11.8236 20.6012C11.5699 21.8719 10.7059 23.1043 9.24253 23.4702C8.94395 23.5448 8.87224 23.6435 8.85319 23.6716C8.82014 23.7204 8.77492 23.8288 8.77233 24.0677C8.7697 24.3108 8.81045 24.5892 8.869 24.9304L8.88606 25.0286L8.88606 25.0286C8.93527 25.3102 9 25.6807 9 26C9 27.4332 8.58821 28.8019 8.09018 29.9113C10.4231 31.2406 13.1229 32 16 32C19.5354 32 22.8029 30.8534 25.4511 28.9117C25.3471 28.7646 25.2399 28.6153 25.1301 28.465C24.5506 27.672 23.9089 26.8682 23.2732 26.2107C22.6167 25.5315 22.05 25.0971 21.6286 24.9285C20.8738 24.6266 20.1566 24.1617 19.9616 23.3144C19.8718 22.9244 19.9228 22.5591 20.0033 22.2613C20.0827 21.9675 20.2083 21.6782 20.3292 21.4199C20.3937 21.2823 20.4611 21.1436 20.5296 21.0026L20.5296 21.0024C20.9898 20.055 21.5 19.0047 21.5 17.5C21.5 16.2061 22.0035 15.2171 22.8218 14.4932C23.5953 13.8089 24.6068 13.3989 25.5877 13.1047C26.3248 12.8835 27.1287 12.7056 27.8822 12.5388L27.8824 12.5388L27.8824 12.5388L27.8824 12.5388C28.1297 12.484 28.3716 12.4305 28.6038 12.377C29.5877 12.1506 30.4202 11.9219 31.0528 11.6056C31.1525 11.5557 31.2566 11.5241 31.3612 11.5094C29.9973 6.83578 26.5583 3.04703 22.1087 1.2075C22.3608 2.30151 22.4695 3.54643 22.4372 4.7763C22.404 6.03545 22.2219 7.31843 21.8678 8.4414C21.5202 9.54349 20.9714 10.6035 20.1247 11.2809C19.7989 11.5415 19.4221 11.7373 18.9722 11.8291C18.5328 11.9188 18.0778 11.8982 17.6057 11.8058C16.6928 11.6273 15.5531 11.1446 14.0528 10.3944C13.4385 10.0873 13.2414 10.1647 13.2267 10.1705L13.2261 10.1707C13.1524 10.1983 13.0653 10.2691 12.7809 10.6247L12.744 10.671C12.5197 10.9534 12.1157 11.4621 11.4761 11.702C10.7349 11.9799 9.92815 11.8321 9.05279 11.3944C8.13774 10.9369 7.56612 10.3552 7.30294 9.57885C7.10968 9.00876 7.1174 8.37679 7.1232 7.90214V7.90213C7.12412 7.82627 7.125 7.75443 7.125 7.6875C7.125 7.13604 7.09486 6.60111 6.87271 5.99018C6.66804 5.42734 6.27867 4.74354 5.50368 3.92379ZM31.8004 13.4655C30.9636 13.8583 29.9823 14.1121 29.0524 14.3261C28.782 14.3883 28.5159 14.4472 28.2543 14.5052C27.5209 14.6676 26.8227 14.8222 26.1623 15.0203C25.2682 15.2886 24.5922 15.5973 24.147 15.9912C23.7465 16.3454 23.5 16.7939 23.5 17.5C23.5 19.496 22.7817 20.9579 22.3162 21.9053L22.3159 21.9058C22.2518 22.0363 22.1925 22.157 22.1405 22.268C22.0298 22.5045 21.9661 22.6645 21.934 22.7831C21.9282 22.8046 21.9241 22.8225 21.9212 22.837C21.9775 22.8824 22.105 22.965 22.3714 23.0715C23.2 23.403 24.0083 24.0935 24.7112 24.8206C25.4349 25.5693 26.1369 26.4531 26.7449 27.285C26.8288 27.3998 26.9111 27.5139 26.9917 27.6269C30.0758 24.7103 32 20.5798 32 16C32 15.1375 31.9318 14.291 31.8004 13.4655ZM6.39285 28.7958C6.7349 27.9612 7 26.9846 7 26C7 25.8639 6.96675 25.6702 6.9058 25.3152L6.8978 25.2686C6.84067 24.9356 6.76762 24.4924 6.77245 24.0461C6.77733 23.5956 6.86084 23.0468 7.19719 22.5502C7.54754 22.0328 8.08591 21.6977 8.75748 21.5299C9.2941 21.3957 9.72071 20.9187 9.86229 20.2096C10.0034 19.5028 9.81659 18.7308 9.2929 18.2071C8.77193 17.6861 8.24794 17.5407 7.6071 17.5693C6.96482 17.598 6.27253 17.7947 5.38824 18.0461L5.38718 18.0464L5.3833 18.0475C5.26594 18.0808 5.1452 18.1152 5.02075 18.1502C3.64971 18.5358 2.00798 18.9412 0.20345 18.5584C0.873856 22.73 3.15691 26.3624 6.39285 28.7958ZM21.8827 22.799C21.882 22.7982 21.8816 22.7979 21.8815 22.7979C21.881 22.798 21.883 22.8012 21.8886 22.8073C21.886 22.8031 21.884 22.8004 21.8827 22.799Z"/></svg>,
dimension_type: <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.2929 22.7071C11.6834 23.0976 12.3166 23.0976 12.7071 22.7071L19.0711 16.3431C19.4616 15.9526 19.4616 15.3195 19.0711 14.9289C18.6805 14.5384 18.0474 14.5384 17.6569 14.9289L13 19.5858V14.1521C14.074 14.1106 15.2545 13.9152 16.5 13.5C17.4051 13.1983 18.0371 12.8511 18.602 12.5408C19.9096 11.8225 20.8576 11.3017 24 12V20C24 22.2091 22.2091 24 20 24H4C1.79086 24 0 22.2091 0 20V9.5C3 7.5 6 10.5 7.5 12.5C7.95659 13.1088 9.22475 13.7863 11 14.0485V19.5858L6.34315 14.9289C5.95262 14.5384 5.31946 14.5384 4.92893 14.9289C4.53841 15.3195 4.53841 15.9526 4.92893 16.3431L11.2929 22.7071ZM11 14.0485V3.41421L6.34315 8.07107C5.95262 8.46159 5.31946 8.46159 4.92893 8.07107C4.53841 7.68054 4.53841 7.04738 4.92893 6.65686L11.2929 0.292893C11.6834 -0.097631 12.3166 -0.097631 12.7071 0.292893L19.0711 6.65686C19.4616 7.04738 19.4616 7.68054 19.0711 8.07107C18.6805 8.46159 18.0474 8.46159 17.6569 8.07107L13 3.41421V14.1521C12.2816 14.1799 11.6108 14.1388 11 14.0485Z"/></svg>,
enchantment: <svg width="31" height="29" viewBox="0 0 31 29" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2.80295 13.5979C2.88259 14.0406 3.03884 14.4773 3.27671 14.8893L8.12207 23.2818C9.22664 25.1949 11.673 25.8504 13.5862 24.7459L26.4471 17.3206C26.842 17.0926 27.1833 16.8075 27.4666 16.4817C27.3507 17.73 26.6514 18.9049 25.4831 19.5795L13.0466 26.7597C11.1334 27.8643 8.68704 27.2088 7.58247 25.2956L2.96224 17.2931C2.2836 16.1177 2.26935 14.741 2.80295 13.5979Z"/><path d="M4.55344 14.1522C3.44887 12.2391 4.10437 9.79271 6.01754 8.68814L17.6017 2C19.5149 0.895427 21.9613 1.55093 23.0658 3.4641L27.1741 10.5798C28.2787 12.493 27.6232 14.9393 25.71 16.0439L14.1258 22.732C12.2126 23.8366 9.76625 23.1811 8.66168 21.2679L4.55344 14.1522Z"/></svg>,
enchantment_provider: <svg width="31" height="29" viewBox="0 0 31 29" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2.80295 13.5979C2.88259 14.0406 3.03884 14.4773 3.27671 14.8893L8.12207 23.2818C9.22664 25.1949 11.673 25.8504 13.5862 24.7459L26.4471 17.3206C26.842 17.0926 27.1833 16.8075 27.4666 16.4817C27.3507 17.73 26.6514 18.9049 25.4831 19.5795L13.0466 26.7597C11.1334 27.8643 8.68704 27.2088 7.58247 25.2956L2.96224 17.2931C2.2836 16.1177 2.26935 14.741 2.80295 13.5979Z"/><path fill-rule="evenodd" clip-rule="evenodd" d="M4.55344 14.1522C3.44887 12.2391 4.10437 9.79271 6.01754 8.68814L17.6017 2C19.5149 0.895427 21.9613 1.55093 23.0658 3.4641L27.1741 10.5798C28.2787 12.493 27.6232 14.9393 25.71 16.0439L14.1258 22.732C12.2126 23.8366 9.76625 23.1811 8.66168 21.2679L4.55344 14.1522ZM14.5294 8.11765C15.0167 7.85775 15.6225 8.0421 15.8824 8.52941L19 14.375L22.1176 8.52941C22.3775 8.0421 22.9833 7.85775 23.4706 8.11765C23.9579 8.37755 24.1423 8.98328 23.8824 9.47059L20.3235 16.1434C19.7588 17.2022 18.2412 17.2022 17.6765 16.1434L14.1176 9.47059C13.8577 8.98328 14.0421 8.37755 14.5294 8.11765ZM12 9C12 8.44771 11.5523 8 11 8C10.4477 8 10 8.44771 10 9L10 16C10 16.5523 10.4477 17 11 17C11.5523 17 12 16.5523 12 16L12 9Z"/></svg>,
equipment: <svg width="20" height="25" viewBox="0 0 20 25" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0.5 14.5C0.1554 13.1216 0.0482983 8.41822 0.0150111 5.62775C-0.000195137 4.35301 0.805687 3.21801 2.00895 2.79687L9.00895 0.346868C9.65057 0.122301 10.3494 0.122301 10.9911 0.346868L17.9911 2.79687C19.1943 3.21801 20.0002 4.35301 19.985 5.62775C19.9517 8.41822 19.8446 13.1216 19.5 14.5C18.6186 18.0257 13.9995 21.7092 11.5419 23.4585C10.6119 24.1204 9.3881 24.1204 8.45812 23.4585C6.0005 21.7092 1.38144 18.0257 0.5 14.5Z"/></svg>,
font: <svg width="26" height="20" viewBox="0 0 26 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 16V3H9V16H3ZM0 1C0 0.447715 0.447715 0 1 0H11C11.5523 0 12 0.447715 12 1V18C12 18.5523 11.5523 19 11 19H1C0.447715 19 0 18.5523 0 18V1ZM17.3224 18.8366C17.8622 19.0687 18.4801 19.1847 19.1761 19.1847C19.6828 19.1847 20.1373 19.1207 20.5398 18.9929C20.947 18.8651 21.2997 18.6733 21.598 18.4176C21.901 18.1619 22.1496 17.8494 22.3438 17.4801H22.429V19H25.696V11.5852C25.696 11.0076 25.5753 10.4938 25.3338 10.044C25.0971 9.59422 24.7609 9.21544 24.3253 8.90767C23.8897 8.59517 23.3783 8.35843 22.7912 8.19744C22.2041 8.03172 21.5625 7.94886 20.8665 7.94886C19.8911 7.94886 19.0554 8.10038 18.3594 8.40341C17.6681 8.7017 17.1236 9.11364 16.7259 9.6392C16.3329 10.16 16.089 10.7519 15.9943 11.4148L19.1974 11.5284C19.2732 11.178 19.4508 10.9034 19.7301 10.7045C20.0095 10.5057 20.3788 10.4062 20.8381 10.4062C21.2642 10.4062 21.6027 10.5057 21.8537 10.7045C22.1046 10.9034 22.2301 11.1851 22.2301 11.5497V11.5852C22.2301 11.8078 22.1425 11.9806 21.9673 12.1037C21.7969 12.2221 21.5223 12.3144 21.1435 12.3807C20.7647 12.4422 20.2652 12.5014 19.6449 12.5582C19.0909 12.6056 18.5701 12.6979 18.0824 12.8352C17.5947 12.9678 17.1638 13.1643 16.7898 13.4247C16.4157 13.6851 16.1222 14.0237 15.9091 14.4403C15.696 14.857 15.5895 15.3684 15.5895 15.9744C15.5895 16.6941 15.7434 17.2931 16.0511 17.7713C16.3636 18.2448 16.7874 18.5999 17.3224 18.8366ZM21.2571 16.6847C20.9588 16.8362 20.6226 16.9119 20.2486 16.9119C19.8509 16.9119 19.5218 16.8172 19.2614 16.6278C19.0057 16.4384 18.8778 16.1638 18.8778 15.804C18.8778 15.5672 18.937 15.3636 19.0554 15.1932C19.1785 15.018 19.3537 14.8759 19.581 14.767C19.813 14.6581 20.0923 14.5777 20.419 14.5256C20.58 14.5019 20.7481 14.4759 20.9233 14.4474C21.0985 14.419 21.2689 14.3859 21.4347 14.348C21.6004 14.3101 21.7519 14.2699 21.8892 14.2273C22.0312 14.1847 22.152 14.1373 22.2514 14.0852V15.1222C22.2514 15.4773 22.1615 15.7898 21.9815 16.0597C21.8016 16.3248 21.5601 16.5331 21.2571 16.6847Z"/></svg>,
instrument: <svg width="28" height="24" viewBox="0 0 28 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.005 1.99915C12.9991 1.44689 12.5523 1 12 1H2C1.44772 1 0.998636 1.44967 1.01243 2.00178C1.32317 14.4349 7.42811 22.5 16 22.5C21.8962 22.5 25.0112 16.6285 26.3167 12.4168C26.6013 11.4989 25.678 11.0504 25.0605 11.7868C23.6857 13.4266 22.0172 15 19 15C13.7789 15 13.0672 7.84982 13.005 1.99915Z" stroke="currentColor" stroke-width="2"/></svg>,
item_modifier: <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg"><line x1="8.05026" y1="17.9498" x2="17.9498" y2="8.05026" stroke="currentColor" stroke-width="4" stroke-linecap="round"/><path d="M10.1749 4.51138C10.1749 4.51138 16.4785 3.75061 19.364 6.63604C22.2494 9.52148 21.4886 15.8251 21.4886 15.8251" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/></svg>,
jukebox_song: <svg width="32" height="21" viewBox="0 0 32 21" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M16 21C24.8366 21 32 16.299 32 10.5C32 4.70101 24.8366 0 16 0C7.16344 0 0 4.70101 0 10.5C0 16.299 7.16344 21 16 21ZM16 13C18.7614 13 21 11.6569 21 10C21 8.34315 18.7614 7 16 7C13.2386 7 11 8.34315 11 10C11 11.6569 13.2386 13 16 13Z"/></svg>,
loot_table: <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0.159296 8.96068C-0.224317 6.7851 1.22836 4.71047 3.40393 4.32686L27.0393 0.159301C29.2149 -0.224312 31.2895 1.22836 31.6731 3.40394L35.8407 27.0393C36.2243 29.2149 34.7716 31.2895 32.5961 31.6731L8.96068 35.8407C6.7851 36.2243 4.71047 34.7716 4.32685 32.5961L0.159296 8.96068ZM11.5999 10.4974C11.9356 12.401 10.6645 14.2163 8.76089 14.552C6.85726 14.8876 5.04196 13.6165 4.7063 11.7129C4.37063 9.80928 5.64172 7.99398 7.54535 7.65832C9.44898 7.32266 11.2643 8.59375 11.5999 10.4974ZM25.5026 11.6C27.4063 11.2643 28.6773 9.44899 28.3417 7.54536C28.006 5.64173 26.1907 4.37064 24.2871 4.7063C22.3835 5.04196 21.1124 6.85727 21.448 8.7609C21.7837 10.6645 23.599 11.9356 25.5026 11.6ZM14.552 27.2391C14.8876 29.1427 13.6165 30.958 11.7129 31.2937C9.80928 31.6294 7.99398 30.3583 7.65831 28.4546C7.32265 26.551 8.59374 24.7357 10.4974 24.4001C12.401 24.0644 14.2163 25.3355 14.552 27.2391ZM28.4546 28.3417C30.3583 28.006 31.6294 26.1907 31.2937 24.2871C30.958 22.3835 29.1427 21.1124 27.2391 21.448C25.3355 21.7837 24.0644 23.599 24.4 25.5026C24.7357 27.4063 26.551 28.6773 28.4546 28.3417Z"/></svg>,
model: <svg width="28" height="23" viewBox="0 0 28 23" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.75 0.27279C13.5437 -0.09095 14.4563 -0.0909509 15.25 0.27279L26.25 5.31446C27.3163 5.8032 28 6.86863 28 8.04165V15.3333C28 16.5444 27.2718 17.6367 26.1538 18.1025L15.1538 22.6859C14.4154 22.9936 13.5846 22.9936 12.8462 22.6859L1.84615 18.1025C0.728214 17.6367 0 16.5444 0 15.3333V8.04165C0 6.86863 0.683678 5.8032 1.75004 5.31446L12.75 0.27279ZM14 4.10002L6.92266 7.3438L14 10.2391L21.0773 7.3438L14 4.10002ZM24 10.4699V14.6666L16 18V13.7427L24 10.4699ZM12 13.7427L4 10.4699V14.6666L12 18V13.7427Z"/></svg>,
pack_mcmeta: <svg width="28" height="31" viewBox="0 0 28 31" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M15.25 0.872837C14.4563 0.509096 13.5437 0.509097 12.75 0.872837L1.75004 5.9145C0.683678 6.40325 0 7.46867 0 8.6417V23.3333C0 24.5444 0.728212 25.6368 1.84615 26.1026L12.8462 30.6859C13.5846 30.9936 14.4154 30.9936 15.1538 30.6859L26.1538 26.1026C27.2718 25.6368 28 24.5444 28 23.3333V8.6417C28 7.46867 27.3163 6.40325 26.25 5.9145L15.25 0.872837ZM11.8374 5.69126L14 4.70006L21.0773 7.94384L18.8225 8.86629L11.8374 5.69126ZM9.4308 6.79428L6.92266 7.94384L14 10.8391L16.2787 9.90694L9.4308 6.79428ZM24 22.6667V11.07L16 14.3427V26L24 22.6667ZM4 11.07L12 14.3427V26L4 22.6667V11.07Z"/></svg>,
painting_variant: <svg width="32" height="28" viewBox="0 0 32 28" fill="none" xmlns="http://www.w3.org/2000/svg"><rect y="7" width="32" height="21" rx="4"/><path d="M9 8L15.2929 1.70711C15.6834 1.31658 16.3166 1.31658 16.7071 1.70711L23 8" fill="none" stroke="currentColor" stroke-width="2"/></svg>,
predicate: <svg width="24" height="22" viewBox="0 0 24 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M22 0H2C1.17595 0 0.705573 0.940764 1.2 1.6L8.8 11.7333C8.92982 11.9064 9 12.117 9 12.3333V20.382C9 21.1253 9.78231 21.6088 10.4472 21.2764L14.4472 19.2764C14.786 19.107 15 18.7607 15 18.382V12.3333C15 12.117 15.0702 11.9064 15.2 11.7333L22.8 1.6C23.2944 0.940764 22.824 0 22 0Z"/></svg>,
recipe: <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20 2H12V10L20 10V2ZM10 2V10H2V4C2 2.89543 2.89543 2 4 2H10ZM2 20L2 12H10V20H2ZM2 22L2 28C2 29.1046 2.89543 30 4 30H10V22H2ZM12 22V30H20V22L12 22ZM22 22V30H28C29.1046 30 30 29.1046 30 28V22H22ZM30 20V12H22V20H30ZM20 20L12 20V12L20 12V20ZM30 4V10H22V2H28C29.1046 2 30 2.89543 30 4ZM4 0C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0H4Z"/></svg>,
'tag/block': TAG,
'tag/damage_type': TAG,
'tag/dialog': TAG,
'tag/enchantment': TAG,
'tag/entity_type': TAG,
'tag/fluid': TAG,
'tag/game_event': TAG,
@@ -35,6 +49,10 @@ export const Icons = {
'tag/painting_variant': TAG,
'tag/point_of_interest_type': TAG,
text_component: <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 4C0 1.79086 1.79086 0 4 0H28C30.2091 0 32 1.79086 32 4V21C32 23.2091 30.2091 25 28 25H17L9.66436 31.5206C9.01946 32.0938 8 31.636 8 30.7732V25H4C1.79086 25 0 23.2091 0 21V4ZM2.46875 20H6.24716L7.20402 16.946H12.2272L13.1861 20H16.9645L12.0568 5.45455H7.37642L2.46875 20ZM11.3888 14.2756L9.76989 9.11932H9.65625L8.04072 14.2756H11.3888ZM19.7521 19.8366C20.2919 20.0687 20.9098 20.1847 21.6058 20.1847C22.1125 20.1847 22.567 20.1207 22.9695 19.9929C23.3767 19.8651 23.7294 19.6733 24.0277 19.4176C24.3307 19.1619 24.5793 18.8494 24.7734 18.4801H24.8587V20H28.1257V12.5852C28.1257 12.0076 28.005 11.4938 27.7635 11.044C27.5268 10.5942 27.1906 10.2154 26.755 9.90767C26.3194 9.59517 25.808 9.35843 25.2209 9.19744C24.6338 9.03172 23.9922 8.94886 23.2962 8.94886C22.3208 8.94886 21.4851 9.10038 20.7891 9.40341C20.0978 9.7017 19.5533 10.1136 19.1555 10.6392C18.7625 11.16 18.5187 11.7519 18.424 12.4148L21.6271 12.5284C21.7029 12.178 21.8804 11.9034 22.1598 11.7045C22.4392 11.5057 22.8085 11.4062 23.2678 11.4062C23.6939 11.4062 24.0324 11.5057 24.2834 11.7045C24.5343 11.9034 24.6598 12.1851 24.6598 12.5497V12.5852C24.6598 12.8078 24.5722 12.9806 24.397 13.1037C24.2266 13.2221 23.9519 13.3144 23.5732 13.3807C23.1944 13.4422 22.6948 13.5014 22.0746 13.5582C21.5206 13.6056 20.9998 13.6979 20.5121 13.8352C20.0244 13.9678 19.5935 14.1643 19.2195 14.4247C18.8454 14.6851 18.5518 15.0237 18.3388 15.4403C18.1257 15.857 18.0192 16.3684 18.0192 16.9744C18.0192 17.6941 18.1731 18.2931 18.4808 18.7713C18.7933 19.2448 19.2171 19.5999 19.7521 19.8366ZM23.6868 17.6847C23.3885 17.8362 23.0523 17.9119 22.6783 17.9119C22.2805 17.9119 21.9515 17.8172 21.6911 17.6278C21.4354 17.4384 21.3075 17.1638 21.3075 16.804C21.3075 16.5672 21.3667 16.3636 21.4851 16.1932C21.6082 16.018 21.7834 15.8759 22.0107 15.767C22.2427 15.6581 22.522 15.5777 22.8487 15.5256C23.0097 15.5019 23.1778 15.4759 23.353 15.4474C23.5282 15.419 23.6986 15.3859 23.8643 15.348C24.0301 15.3101 24.1816 15.2699 24.3189 15.2273C24.4609 15.1847 24.5817 15.1373 24.6811 15.0852V16.1222C24.6811 16.4773 24.5911 16.7898 24.4112 17.0597C24.2313 17.3248 23.9898 17.5331 23.6868 17.6847Z"/></svg>,
trial_spawner: <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M28 2H17V5L30 5V4C30 2.89543 29.1046 2 28 2ZM30 7L17 7V10H18C18.7403 10 19.3866 10.4022 19.7324 11H30V7ZM20 18V13H30V18H20ZM17 21H18C18.7403 21 19.3866 20.5978 19.7324 20H30V25H17V21ZM12.2676 20C12.6134 20.5978 13.2597 21 14 21H15V25H2L2 20H12.2676ZM12 13V18H2L2 13H12ZM15 10H14C13.2597 10 12.6134 10.4022 12.2676 11H2L2 7L15 7V10ZM15 27H2L2 28C2 29.1046 2.89543 30 4 30H15V27ZM17 27H30V28C30 29.1046 29.1046 30 28 30H17V27ZM15 5V2H4C2.89543 2 2 2.89543 2 4V5L15 5ZM4 0C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0H4Z"/></svg>,
trim_material: <svg width="28" height="22" viewBox="0 0 28 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.6805 0.631848C14.2968 0.275572 13.7032 0.275572 13.3195 0.631847L0.443587 12.5881C0.170896 12.8413 0.0579916 13.23 0.179682 13.5817C3.71776 23.8061 24.2822 23.8061 27.8203 13.5817C27.942 13.23 27.8291 12.8413 27.5564 12.5881L14.6805 0.631848Z"/></svg>,
trim_pattern: <svg width="22" height="28" viewBox="0 0 22 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.55446 0.223188C6.0161 0.0999245 5.47976 0.436422 5.35649 0.974776L0.223178 23.3946C0.0999154 23.933 0.436413 24.4693 0.974766 24.5926L15.5964 27.9404C16.1348 28.0637 16.6711 27.7272 16.7944 27.1888L20.3654 11.5924L10.6176 9.36052C10.0793 9.23725 9.74276 8.70091 9.86602 8.16255L10.0892 7.18778C10.2125 6.64943 10.7488 6.31293 11.2872 6.43619L21.0349 8.66807L21.9277 4.76897C22.0509 4.23061 21.7144 3.69427 21.1761 3.571L6.55446 0.223188ZM4.85098 12.1437C4.97424 11.6053 5.51059 11.2688 6.04894 11.3921L14.8219 13.4008C15.3603 13.524 15.6968 14.0604 15.5735 14.5987L15.3503 15.5735C15.2271 16.1119 14.6907 16.4484 14.1524 16.3251L5.37938 14.3164C4.84103 14.1932 4.50453 13.6568 4.62779 13.1185L4.85098 12.1437ZM6.65937 17.6871C6.12102 17.5639 5.58467 17.9004 5.46141 18.4387L5.23822 19.4135C5.11496 19.9518 5.45145 20.4882 5.98981 20.6114L14.7628 22.6201C15.3011 22.7434 15.8375 22.4069 15.9607 21.8685L16.1839 20.8938C16.3072 20.3554 15.9707 19.8191 15.4323 19.6958L6.65937 17.6871Z"/></svg>,
wolf_variant: <svg width="29" height="27" viewBox="0 0 29 27" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 7C12.3431 7 11 8.34315 11 10V24C11 25.6569 12.3431 27 14 27H26C27.6569 27 29 25.6569 29 24V10C29 8.34315 27.6569 7 26 7H14ZM14 12C12.8954 12 12 12.8954 12 14V15C12 16.1046 12.8954 17 14 17H15C16.1046 17 17 16.1046 17 15V14C17 12.8954 16.1046 12 15 12H14Z"/><path d="M0 20C0 18.3431 1.34315 17 3 17H9V26H3C1.34315 26 0 24.6569 0 23V20Z"/><rect x="22" width="4" height="13" rx="2"/></svg>,
world: <svg width="28" height="29" viewBox="0 0 28 29" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6 2C6 0.895431 6.89543 0 8 0H10C11.1046 0 12 0.895431 12 2V4C12 5.10457 11.1046 6 10 6V10.5688C10.2875 10.9298 10.5816 11.3478 10.8939 11.7915L10.8939 11.7916C11.5 12.6529 12.1742 13.6111 13 14.4368C13.8636 15.3004 15.3471 15.813 17 15.9652V12C15.8954 12 15 11.1046 15 10V8C15 6.89543 15.8954 6 17 6H19C20.1046 6 21 6.89543 21 8V10C21 11.1046 20.1046 12 19 12V15.8819C20.1705 15.6766 21.362 15.2558 22 14.9368C23 14.4368 26 14 28 14.4368V20C28 22.2091 26.2091 24 24 24H8C5.79086 24 4 22.2091 4 20V9C5.38919 8.65271 6.77837 8.54664 8 9.01691V6C6.89543 6 6 5.10457 6 4V2ZM22 26.2968H6C3.79086 26.2968 2 24.5059 2 22.2968V13C1.34604 13.0169 0.673018 13.1285 0 13.2968V24.2968C0 26.5059 1.79086 28.2968 4 28.2968H20C21.6222 28.2968 23.0189 27.3311 23.6465 25.9433C23.1444 26.1704 22.5869 26.2968 22 26.2968Z"/></svg>,
worldgen: <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 0C2.89543 0 2 0.89543 2 2V4C2 5.10457 2.89543 6 4 6V9.01691C2.77837 8.54664 1.38919 8.65271 0 9V20C0 22.2091 1.79086 24 4 24H20C22.2091 24 24 22.2091 24 20V14.4368C22 14 19 14.4368 18 14.9368C17.362 15.2558 16.1705 15.6766 15 15.8819V12C16.1046 12 17 11.1046 17 10V8C17 6.89543 16.1046 6 15 6H13C11.8954 6 11 6.89543 11 8V10C11 11.1046 11.8954 12 13 12V15.9652C11.3471 15.813 9.86362 15.3004 9 14.4368C8.17424 13.6111 7.50001 12.6529 6.8939 11.7916L6.89388 11.7916L6.89388 11.7916L6.89385 11.7915C6.58163 11.3478 6.28748 10.9298 6 10.5688V6C7.10457 6 8 5.10457 8 4V2C8 0.895431 7.10457 0 6 0H4Z"/></svg>,
'worldgen/biome': <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 1C7 0.447715 7.44772 0 8 0C8.55229 0 9 0.447715 9 1V3C9 3.55228 8.55229 4 8 4C7.44772 4 7 3.55228 7 3V1ZM20.5 21H22.5C23.2136 21 23.6975 20.2741 23.4231 19.6154L19.9231 11.2154C19.5812 10.3949 18.4188 10.3949 18.0769 11.2154L14.5769 19.6154C14.3025 20.2741 14.7864 21 15.5 21H17.5V24H20.5V21ZM16 8C16 7.44772 15.5523 7 15 7H13C12.4477 7 12 7.44772 12 8C12 8.55228 12.4477 9 13 9H15C15.5523 9 16 8.55229 16 8ZM14.4142 13C14.8047 13.3905 14.8047 14.0237 14.4142 14.4142C14.0237 14.8047 13.3905 14.8047 13 14.4142L11.5858 13C11.1953 12.6095 11.1953 11.9763 11.5858 11.5858C11.9763 11.1953 12.6095 11.1953 13 11.5858L14.4142 13ZM4 8C4 7.44772 3.55228 7 3 7H1C0.447715 7 0 7.44772 0 8C0 8.55228 0.447715 9 1 9H3C3.55228 9 4 8.55229 4 8ZM4.41422 3C4.80474 3.39053 4.80474 4.02369 4.41422 4.41421C4.02369 4.80474 3.39053 4.80474 3 4.41421L1.58579 3C1.19526 2.60948 1.19526 1.97631 1.58579 1.58579C1.97631 1.19526 2.60948 1.19526 3 1.58579L4.41422 3ZM8 16C8.55229 16 9 15.5523 9 15V13C9 12.4477 8.55229 12 8 12C7.44772 12 7 12.4477 7 13L7 15C7 15.5523 7.44772 16 8 16ZM3 14.4142C2.60947 14.8047 1.97631 14.8047 1.58579 14.4142C1.19526 14.0237 1.19526 13.3905 1.58579 13L3 11.5858C3.39052 11.1953 4.02369 11.1953 4.41421 11.5858C4.80474 11.9763 4.80474 12.6095 4.41421 13L3 14.4142ZM14.4142 1.58579C14.0237 1.19526 13.3905 1.19526 13 1.58579L11.5858 3C11.1953 3.39053 11.1953 4.02369 11.5858 4.41422C11.9763 4.80474 12.6095 4.80474 13 4.41422L14.4142 3C14.8047 2.60948 14.8047 1.97631 14.4142 1.58579ZM7 5C5.89543 5 5 5.89543 5 7V9C5 10.1046 5.89543 11 7 11H9C10.1046 11 11 10.1046 11 9V7C11 5.89543 10.1046 5 9 5H7Z"/></svg>,

View File

@@ -1,13 +1,13 @@
import type { ItemStack } from 'deepslate/core'
import { Identifier } from 'deepslate/core'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { useVersion } from '../contexts/Version.jsx'
import { useAsync } from '../hooks/useAsync.js'
import { fetchItemComponents } from '../services/index.js'
import { ResolvedItem } from '../services/ResolvedItem.js'
import { renderItem } from '../services/Resources.js'
import { getCollections } from '../services/Schemas.js'
import { jsonToNbt } from '../Utils.js'
import { ItemTooltip } from './ItemTooltip.jsx'
import { Octicon } from './Octicon.jsx'
import { itemHasGlint } from './previews/LootTable.js'
interface Props {
item: ItemStack,
@@ -16,6 +16,7 @@ interface Props {
advancedTooltip?: boolean,
}
export function ItemDisplay({ item, slotDecoration, tooltip, advancedTooltip }: Props) {
const { version } = useVersion()
const el = useRef<HTMLDivElement>(null)
const [tooltipOffset, setTooltipOffset] = useState<[number, number]>([0, 0])
const [tooltipSwap, setTooltipSwap] = useState(false)
@@ -33,10 +34,20 @@ export function ItemDisplay({ item, slotDecoration, tooltip, advancedTooltip }:
return () => el.current?.removeEventListener('mousemove', onMove)
}, [])
const maxDamage = item.getItem().durability
const { value: baseComponents } = useAsync(() => fetchItemComponents(version), [version])
const itemResolver = useCallback((item: ItemStack) => {
const base = baseComponents?.get(item.id.toString()) ?? new Map()
return new ResolvedItem(item, new Map([...base.entries()].map(([k, v]) => [k, jsonToNbt(v)])))
}, [baseComponents])
const resolvedItem = useMemo(() => {
return itemResolver(item)
}, [item, itemResolver])
const maxDamage = resolvedItem.getMaxDamage()
const damage = resolvedItem.getDamage()
return <div class="item-display" ref={el}>
<ItemItself item={item} />
<RenderedItem item={resolvedItem} baseComponents={baseComponents} />
{item.count !== 1 && <>
<svg class="item-count" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMinYMid meet">
<text x="95" y="93" font-size="50" textAnchor="end" fontFamily="MinecraftSeven" fill="#373737">{item.count}</text>
@@ -44,53 +55,39 @@ export function ItemDisplay({ item, slotDecoration, tooltip, advancedTooltip }:
</svg>
</>}
{slotDecoration && <>
{(maxDamage && item.tag.getNumber('Damage') > 0) && <svg class="item-durability" width="100%" height="100%" viewBox="0 0 18 18">
{(maxDamage > 0 && damage > 0) && <svg class="item-durability" width="100%" height="100%" viewBox="0 0 18 18">
<rect x="3" y="14" width="13" height="2" fill="#000" />
<rect x="3" y="14" width={`${(maxDamage - item.tag.getNumber('Damage')) / maxDamage * 13}`} height="1" fill={`hsl(${(maxDamage - item.tag.getNumber('Damage')) / maxDamage * 120}deg, 100%, 50%)`} />
<rect x="3" y="14" width={`${(maxDamage - damage) / maxDamage * 13}`} height="1" fill={`hsl(${(maxDamage - damage) / maxDamage * 120}deg, 100%, 50%)`} />
</svg>}
<div class="item-slot-overlay"></div>
</>}
{tooltip !== false && <div class="item-tooltip" style={tooltipOffset && {
{tooltip !== false && !resolvedItem.has('hide_tooltip') && <div class="item-tooltip" style={tooltipOffset && {
left: (tooltipSwap ? undefined : `${tooltipOffset[0]}px`),
right: (tooltipSwap ? `${tooltipOffset[0]}px` : undefined),
top: `${tooltipOffset[1]}px`,
}}>
<ItemTooltip item={item} advanced={advancedTooltip} />
<ItemTooltip item={resolvedItem} advanced={advancedTooltip} resolver={itemResolver} />
</div>}
</div>
}
function ItemItself({ item }: Props) {
const { version } = useVersion()
const hasGlint = itemHasGlint(item)
if (item.id.namespace !== Identifier.DEFAULT_NAMESPACE) {
return Octicon.package
}
const { value: collections } = useAsync(() => getCollections(version), [])
if (collections === undefined) {
return null
}
const modelPath = `item/${item.id.path}`
if (collections.get('model').includes('minecraft:' + modelPath)) {
return <RenderedItem item={item} hasGlint={hasGlint} />
}
return Octicon.package
interface ResolvedProps extends Props {
item: ResolvedItem
baseComponents: Map<string, Map<string, unknown>> | undefined
}
function RenderedItem({ item, hasGlint }: Props & { hasGlint: boolean }) {
function RenderedItem({ item, baseComponents }: ResolvedProps) {
const { version } = useVersion()
const { value: src } = useAsync(() => renderItem(version, item), [version, item])
const { value: src } = useAsync(async () => {
if (!baseComponents) {
return undefined
}
return renderItem(version, item, baseComponents)
}, [version, item, baseComponents])
if (src) {
return <>
<img src={src} alt={item.id.toString()} class="model" draggable={false} />
{hasGlint && <div class="item-glint" style={{'--mask-image': `url("${src}")`}}></div>}
{item.hasFoil() && <div class="item-glint" style={{'--mask-image': `url("${src}")`}}></div>}
</>
}

View File

@@ -0,0 +1,103 @@
import type { ItemStack } from 'deepslate-1.20.4/core'
import { Identifier } from 'deepslate-1.20.4/core'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useVersion } from '../contexts/Version.jsx'
import { useAsync } from '../hooks/useAsync.js'
import { fetchRegistries } from '../services/index.js'
import { renderItem } from '../services/Resources1204.js'
import { ItemTooltip1204 } from './ItemTooltip1204.jsx'
import { Octicon } from './Octicon.jsx'
import { itemHasGlint } from './previews/LootTable1204.js'
interface Props {
item: ItemStack,
slotDecoration?: boolean,
tooltip?: boolean,
advancedTooltip?: boolean,
}
export function ItemDisplay1204({ item, slotDecoration, tooltip, advancedTooltip }: Props) {
const el = useRef<HTMLDivElement>(null)
const [tooltipOffset, setTooltipOffset] = useState<[number, number]>([0, 0])
const [tooltipSwap, setTooltipSwap] = useState(false)
useEffect(() => {
const onMove = (e: MouseEvent) => {
requestAnimationFrame(() => {
const { right, width } = el.current!.getBoundingClientRect()
const swap = right + 200 > document.body.clientWidth
setTooltipSwap(swap)
setTooltipOffset([(swap ? width - e.offsetX : e.offsetX) + 20, e.offsetY - 40])
})
}
el.current?.addEventListener('mousemove', onMove)
return () => el.current?.removeEventListener('mousemove', onMove)
}, [])
const maxDamage = item.getItem().durability
return <div class="item-display" ref={el}>
<ItemItself item={item} />
{item.count !== 1 && <>
<svg class="item-count" width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="xMinYMid meet">
<text x="95" y="93" font-size="50" textAnchor="end" fontFamily="MinecraftSeven" fill="#373737">{item.count}</text>
<text x="90" y="88" font-size="50" textAnchor="end" fontFamily="MinecraftSeven" fill="#ffffff">{item.count}</text>
</svg>
</>}
{slotDecoration && <>
{(maxDamage && item.tag.getNumber('Damage') > 0) && <svg class="item-durability" width="100%" height="100%" viewBox="0 0 18 18">
<rect x="3" y="14" width="13" height="2" fill="#000" />
<rect x="3" y="14" width={`${(maxDamage - item.tag.getNumber('Damage')) / maxDamage * 13}`} height="1" fill={`hsl(${(maxDamage - item.tag.getNumber('Damage')) / maxDamage * 120}deg, 100%, 50%)`} />
</svg>}
<div class="item-slot-overlay"></div>
</>}
{tooltip !== false && <div class="item-tooltip" style={tooltipOffset && {
left: (tooltipSwap ? undefined : `${tooltipOffset[0]}px`),
right: (tooltipSwap ? `${tooltipOffset[0]}px` : undefined),
top: `${tooltipOffset[1]}px`,
}}>
<ItemTooltip1204 item={item} advanced={advancedTooltip} />
</div>}
</div>
}
function ItemItself({ item }: Props) {
const { version } = useVersion()
const hasGlint = itemHasGlint(item)
if (item.id.namespace !== Identifier.DEFAULT_NAMESPACE) {
return Octicon.package
}
const { value: allModels, loading: loadingModels } = useAsync(async () => {
const registries = await fetchRegistries(version)
return registries.get('model')
}, [version])
if (loadingModels || allModels === undefined) {
return null
}
const modelPath = `item/${item.id.path}`
if (allModels && allModels.includes('minecraft:' + modelPath)) {
return <RenderedItem item={item} hasGlint={hasGlint} />
}
return Octicon.package
}
function RenderedItem({ item, hasGlint }: Props & { hasGlint: boolean }) {
const { version } = useVersion()
const { value: src } = useAsync(() => renderItem(version, item), [version, item])
if (src) {
return <>
<img src={src} alt={item.id.toString()} class="model" draggable={false} />
{hasGlint && <div class="item-glint" style={{'--mask-image': `url("${src}")`}}></div>}
</>
}
return <div class="item-display">
{Octicon.package}
</div>
}

View File

@@ -1,135 +1,296 @@
import type { ItemStack } from 'deepslate/core'
import { AttributeModifierOperation, Enchantment, Identifier, MobEffectInstance, Potion } from 'deepslate/core'
import { NbtList, NbtType } from 'deepslate/nbt'
import { message } from '../Utils.js'
import type { MobEffectInstance, NbtTag } from 'deepslate'
import { ItemStack, NbtCompound, NbtList, PotionContents } from 'deepslate'
import { Identifier } from 'deepslate/core'
import { useVersion } from '../contexts/Version.jsx'
import { useAsync } from '../hooks/useAsync.js'
import { getLanguage, getTranslation } from '../services/Resources.js'
import type { ResolvedItem } from '../services/ResolvedItem.js'
import { intToDisplayHexRgb, makeDescriptionId, mergeTextComponentStyles } from '../Utils.js'
import { TextComponent } from './TextComponent.jsx'
interface Props {
item: ItemStack,
item: ResolvedItem,
advanced?: boolean,
resolver: (item: ItemStack) => ResolvedItem,
}
export function ItemTooltip({ item, advanced }: Props) {
export function ItemTooltip({ item, advanced, resolver }: Props) {
const { version } = useVersion()
const { value: language } = useAsync(() => getLanguage(version), [version])
const isPotion = item.is('potion') || item.is('splash_potion') || item.is('lingering_potion')
let displayName = item.tag.getCompound('display').getString('Name')
let name: string | undefined
if (displayName) {
try {
name = JSON.parse(displayName)
} catch (e) {
console.warn(`Error parsing display name '${displayName}': ${message(e)}`)
displayName = ''
}
}
if (name === undefined) {
if (language) {
let descriptionId = `${item.id.namespace}.${item.id.path}`
if (isPotion) {
descriptionId = `${descriptionId}.effect.${Potion.fromNbt(item).name}`
}
name = getTranslation(language, `item.${descriptionId}`)
name ??= getTranslation(language, `block.${descriptionId}`)
}
name ??= item.id.path
.replace(/[_\/]/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
const lore: any[] = []
item.tag.getCompound('display').getList('Lore', NbtType.String).forEach((line) => {
try {
lore.push(JSON.parse(line['value']))
} catch (e) {
console.warn(`Error parsing lore line '${line}': ${message(e)}`)
}
})
const durability = item.getItem().durability
const enchantments = (item.is('enchanted_book') ? item.tag.getList('StoredEnchantments', NbtType.Compound) : item.tag.getList('Enchantments', NbtType.Compound)) ?? NbtList.create()
const effects = isPotion ? Potion.getAllEffects(item) : []
const attributeModifiers = isPotion ? Potion.getAllAttributeModifiers(item) : []
return <>
<TextComponent component={name} base={{ color: 'white', italic: displayName.length > 0 }} />
{shouldShow(item, 'additional') && <>
{(!advanced && displayName.length === 0 && item.is('filled_map') && item.tag.hasNumber('map')) && <>
<TextComponent component={{ text: `#${item.tag.getNumber('map')}`, color: 'gray' }} />
<TextComponent component={item.getStyledHoverName(version)} />
{!advanced && !item.has('custom_name') && item.is('filled_map') && item.has('map_id') && (
<TextComponent component={{ translate: 'filled_map.id', with: [item.get('map_id', tag => tag.getAsNumber())], color: 'gray' }} />
)}
{!item.has('hide_additional_tooltip') && <>
{item.is('filled_map') && advanced && (item.get('map_id', tag => tag.isNumber())
? <TextComponent component={{ translate: 'filled_map.id', with: [item.get('map_id', tag => tag.getAsNumber())], color: 'gray' }} />
: <TextComponent component={{ translate: 'filled_map.unknown', color: 'gray' }} />
)}
{(item.id.path.endsWith('_banner') || item.is('shield')) && item.get('banner_patterns', tag => tag.isList() ? tag : [])?.map(layer =>
<TextComponent component={{ translate: `${layer.isCompound() ? (layer.hasCompound('pattern') ? layer.getString('translation_key') : `block.minecraft.banner.${layer.getString('pattern').replace(/^minecraft:/, '')}`) : ''}.${layer.isCompound() ? layer.getString('color') : ''}`, color: 'gray' }} />
)}
{item.is('crossbow') && item.getChargedProjectile() && (
<TextComponent component={{ translate: 'item.minecraft.crossbow.projectile', extra: [' ', resolver(item.getChargedProjectile()!).getDisplayName(version)] }}/>
)}
{item.is('disc_fragment_5') && (
<TextComponent component={{ translate: `${makeDescriptionId('item', item.id)}.desc`, color: 'gray' }} />
)}
{item.is('firework_rocket') && item.has('fireworks') && <>
{((item.get('fireworks', tag => tag.isCompound() ? tag.getNumber('flight_duration') : 0) ?? 0) > 0) && (
<TextComponent component={{ translate: 'item.minecraft.firework_rocket.flight', extra: [' ', item.get('fireworks', tag => tag.isCompound() ? tag.getNumber('flight_duration') : 0)], color: 'gray'}} />
)}
{/* TODO: firework explosions */}
</>}
{(item.is('filled_map') && advanced) && <>
<TextComponent component={{ translate: 'filled_map.unknown', color: 'gray' }} />
{item.is('firework_star') && item.has('firework_explosion') && (
<TextComponent component={{ translate: `item.minecraft.firework_star.shape.${item.get('firework_explosion', tag => tag.isCompound() ? tag.getString('shape') : '')}`, color: 'gray' }} />
// TODO: additional stuff
)}
{/* TODO: painting variants */}
{item.is('goat_horn') && item.has('instrument') && (
<TextComponent component={mergeTextComponentStyles(item.get('instrument', tag => tag.isCompound()
? tag.get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('instrument', Identifier.parse(tag.getAsString()))}
), { color: 'gray' })} />
)}
{(item.is('lingering_potion') || item.is('potion') || item.is('splash_potion') || item.is('tipped_arrow')) && (
<PotionContentsTooltip contents={PotionContents.fromNbt(item.get('potion_contents', tag => tag) ?? NbtCompound.create())} factor={item.is('lingering_potion') ? 0.25 : item.is('tipped_arrow') ? 0.125 : 1} />
)}
{/* TODO: mob buckets */}
{/* TODO: smithing templates */}
{item.is('written_book') && item.has('written_book_content') && <>
<TextComponent component={{ translate: 'book.byAuthor', with: [item.get('written_book_content', tag => tag.isCompound() ? tag.getString('author') : undefined) ?? ''], color: 'gray' }} />
<TextComponent component={{ translate: `book.generation.${item.get('written_book_content', tag => tag.isCompound() ? tag.getNumber('generation') : undefined) ?? 0}`, color: 'gray' }} />
</>}
{isPotion && effects.length === 0
? <TextComponent component={{ translate: 'effect.none', color: 'gray' }} />
: effects.map(e => {
const color = e.effect.category === 'harmful' ? 'red' : 'blue'
let component: any = { translate: `effect.${e.effect.id.namespace}.${e.effect.id.path}` }
if (e.amplifier > 0) {
component = { translate: 'potion.withAmplifier', with: [component, { translate: `potion.potency.${e.amplifier}` }] }
}
if (e.duration > 20) {
component = { translate: 'potion.withDuration', with: [component, MobEffectInstance.formatDuration(e)] }
}
return <TextComponent component={{ ...component, color }} />
})}
{attributeModifiers.length > 0 && <>
<TextComponent component='' />
<TextComponent component={{ translate: 'potion.whenDrank', color: 'dark_purple' }} />
{attributeModifiers.map(([attr, { amount, operation }]) => {
const a = operation === AttributeModifierOperation.addition ? amount * 100 : amount
if (amount > 0) {
return <TextComponent component={{ translate: `attribute.modifier.plus.${operation}`, with: [Math.floor(a * 100) / 100, { translate: `attribute.name.${attr.id.path}` }], color: 'blue' }} />
} else if (amount < 0) {
return <TextComponent component={{ translate: `attribute.modifier.take.${operation}`, with: [Math.floor(a * -100) / 100, { translate: `attribute.name.${attr.id.path}` }], color: 'red' }} />
}
return null
{(item.is('beehive') || item.is('bee_nest')) && <>
<TextComponent component={{ translate: 'container.beehive.bees', with: [item.get('bees', tag => tag.isList() ? tag.length : 0) ?? 0, 3], color: 'gray' }} />
<TextComponent component={{ translate: 'container.beehive.honey', with: [item.get('block_state', tag => tag.isCompound() ? tag.getString('honey_level') : 0) ?? 0, 5], color: 'gray' }} />
</>}
{item.is('decorated_pot') && item.has('pot_decorations') && <>
<TextComponent component={''} />
{item.get('pot_decorations', tag => tag.isList() ? tag.map(e =>
<TextComponent component={mergeTextComponentStyles(resolver(new ItemStack(Identifier.parse(e.getAsString()), 1)).getHoverName(version), { color: 'gray' })} />
) : undefined)}
</>}
{item.id.path.endsWith('_shulker_box') && <>
{item.has('container_loot') && (
<TextComponent component={{ translate: 'container.shulkerBox.unknownContents' }} />
)}
{(item.get('container', tag => tag.isList() ? tag.getItems() : []) ?? []).slice(0, 5).map(e => {
const subItem = resolver(ItemStack.fromNbt(e.isCompound() ? e.getCompound('item') : new NbtCompound()))
return <TextComponent component={{ translate: 'container.shulkerBox.itemCount', with: [subItem.getHoverName(version), subItem.count] }} />
})}
{(item.get('container', tag => tag.isList() ? tag.length : 0) ?? 0) > 5 && (
<TextComponent component={{ translate: 'container.shulkerBox.more', with: [(item.get('container', tag => tag.isList() ? tag.length : 0) ?? 0) - 5], italic: true }} />
)}
</>}
{/* TODO: spawner and trial spawner */}
</>}
{shouldShow(item, 'enchantments') && enchantments.map(enchantment => {
const id = enchantment.getString('id')
const lvl = enchantment.getNumber('lvl')
const ench = Enchantment.REGISTRY.get(Identifier.parse(id))
const component: any[] = [{ translate: `enchantment.${id.replace(':', '.')}`, color: ench?.isCurse ? 'red' : 'gray' }]
if (lvl !== 1 || ench?.maxLevel !== 1) {
component.push(' ', { translate: `enchantment.level.${lvl}`})
}
return <TextComponent component={component} />
})}
{item.tag.hasCompound('display') && <>
{shouldShow(item, 'dye') && item.tag.getCompound('display').hasNumber('color') && (advanced
? <TextComponent component={{ translate: 'item.color', with: [`#${item.tag.getCompound('display').getNumber('color').toString(16).padStart(6, '0')}`], color: 'gray' }} />
: <TextComponent component={{ translate: 'item.dyed', color: 'gray' }} />)}
{lore.map((component) => <TextComponent component={component} base={{ color: 'dark_purple', italic: true }} />)}
{item.showInTooltip('jukebox_playable') && <>
<TextComponent component={mergeTextComponentStyles(item.get('jukebox_playable', tag => tag.isCompound() ? (
tag.hasCompound('song')
? tag.getCompound('song').get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('jukebox_song', Identifier.parse(tag.getString('song')))}
) : {}) ?? {}, { color: 'gray'})} />
</>}
{shouldShow(item, 'unbreakable') && item.tag.getBoolean('Unbreakable') && <TextComponent component={{ translate: 'item.unbreakable', color: 'blue' }} />}
{(advanced && item.tag.getNumber('Damage') > 0 && durability) && <TextComponent component={{ translate: 'item.durability', with: [`${durability - item.tag.getNumber('Damage')}`, `${durability}`] }} />}
{item.showInTooltip('trim') && <>
<TextComponent component={{ translate: makeDescriptionId('item', Identifier.create('smithing_template.upgrade' )), color: 'gray' }} />
<TextComponent component={{ text: ' ', extra: [item.get('trim', tag => tag.isCompound() ? (
tag.hasCompound('pattern')
? tag.getCompound('pattern').get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('trim_pattern', Identifier.parse(tag.getString('pattern'))), color: BUILTIN_TRIM_MATERIALS[tag.getString('material').replace(/^minecraft:/, '')] ?? 'gray' }
) : '')] }} />
<TextComponent component={{ text: ' ', extra: [item.get('trim', tag => tag.isCompound() ? (
tag.hasCompound('material')
? tag.getCompound('material').get('description')?.toSimplifiedJson()
: { translate: makeDescriptionId('trim_material', Identifier.parse(tag.getString('material'))), color: BUILTIN_TRIM_MATERIALS[tag.getString('material').replace(/^minecraft:/, '')] ?? 'gray' }
) : '')] }}/>
</>}
{item.showInTooltip('stored_enchantments') && (
<EnchantmentsTooltip data={item.get('stored_enchantments', tag => tag)} />
)}
{item.showInTooltip('enchantments') && (
<EnchantmentsTooltip data={item.get('enchantments', tag => tag)} />
)}
{item.showInTooltip('dyed_color') && (advanced
? <TextComponent component={{ translate: 'item.color', with: [intToDisplayHexRgb(item.get('dyed_color', tag => tag.isCompound() ? tag.getNumber('rgb') : tag.getAsNumber()))], color: 'gray' }} />
: <TextComponent component={{ translate: 'item.dyed', color: 'gray' }} />
)}
{item.getLore(version).map((component) =>
<TextComponent component={component} base={{ color: 'dark_purple', italic: true }} />
)}
{item.showInTooltip('attribute_modifiers') && (
<AttributeModifiersTooltip data={item.get('attribute_modifiers', tag => tag)} />
)}
{item.showInTooltip('unbreakable') && (
<TextComponent component={{ translate: 'item.unbreakable', color: 'blue' }} />
)}
{item.has('ominous_bottle_amplifier') && (
<PotionContentsTooltip contents={{ customEffects: [{ effect: Identifier.create('bad_omen'), amplifier: item.get('ominous_bottle_amplifier', tag => tag.getAsNumber()) ?? 0, duration: 120000 }]}} />
)}
{/* TODO: creative-only suspicious stew effects */}
{/* TODO: can break and can place on */}
{advanced && item.isDamageable() && (
<TextComponent component={{ translate: 'item.durability', with: [`${item.getMaxDamage() - item.getDamage()}`, `${item.getMaxDamage()}`] }} />
)}
{advanced && <>
<TextComponent component={{ text: item.id.toString(), color: 'dark_gray'}} />
{item.tag.size > 0 && <TextComponent component={{ translate: 'item.nbt_tags', with: [item.tag.size], color: 'dark_gray' }} />}
{item.getSize() > 0 && <TextComponent component={{ translate: 'item.components', with: [item.getSize()], color: 'dark_gray' }} />}
</>}
</>
}
const TooltipMasks = {
enchantments: 1,
modifiers: 2,
unbreakable: 4,
can_destroy: 8,
can_place: 16,
additional: 32,
dye: 64,
upgrades: 128,
const BUILTIN_TRIM_MATERIALS: Record<string, string | undefined> = {
amethyst: '#9A5CC6',
copper: '#B4684D',
diamond: '#6EECD2',
emerald: '#11A036',
gold: '#DEB12D',
iron: '#ECECEC',
lapis: '#416E97',
netherite: '#625859',
quartz: '#E3D4C4',
redstone: '#971607',
}
function shouldShow(item: ItemStack, mask: keyof typeof TooltipMasks) {
const flags = item.tag.getNumber('HideFlags')
return (flags & TooltipMasks[mask]) === 0
const HARMFUL_EFFECTS = new Set([
'minecraft:slowness',
'minecraft:mining_fatigue',
'minecraft:instant_damage',
'minecraft:nausea',
'minecraft:blindness',
'minecraft:hunger',
'minecraft:weakness',
'minecraft:poison',
'minecraft:wither',
'minecraft:levitation',
'minecraft:unluck',
'minecraft:darkness',
'minecraft:wind_charged',
'minecraft:weaving',
'minecraft:oozing',
'minecraft:infested',
])
function PotionContentsTooltip({ contents, factor }: { contents: PotionContents, factor?: number }) {
const effects = PotionContents.getAllEffects(contents)
return <>
{effects.map(e => {
const color = HARMFUL_EFFECTS.has(e.effect.toString()) ? 'red' : 'blue'
let component: any = { translate: makeDescriptionId('effect', e.effect) }
if (e.amplifier > 0) {
component = { translate: 'potion.withAmplifier', with: [component, { translate: `potion.potency.${e.amplifier}` }] }
}
if (e.duration === -1 || e.duration > 20) {
component = { translate: 'potion.withDuration', with: [component, formatDuration(e, factor ?? 1)] }
}
return <TextComponent component={{ ...component, color }} />
})}
{effects.length === 0 && <TextComponent component={{ translate: 'effect.none', color: 'gray' }} />}
</>
}
function formatDuration(effect: MobEffectInstance, factor: number) {
if (effect.duration === -1) {
return { translate: 'effect.duration.infinite' }
}
const ticks = Math.floor(effect.duration * factor)
let seconds = Math.floor(ticks / 20)
let minutes = Math.floor(seconds / 60)
seconds %= 60
const hours = Math.floor(minutes / 60)
minutes %= 60
return `${hours > 0 ? `${hours}:` : ''}${minutes.toFixed().padStart(2, '0')}:${seconds.toFixed().padStart(2, '0')}`
}
function EnchantmentsTooltip({ data }: { data: NbtTag | undefined }) {
if (!data || !data.isCompound()) {
return <></>
}
const levels = data.hasCompound('levels') ? data.getCompound('levels') : data
return <>
{[...levels.keys()].map((key) => {
const level = levels.getNumber(key)
if (level <= 0) return <></>
const id = Identifier.parse(key)
return <TextComponent component={{ translate: makeDescriptionId('enchantment', id), color: id.path.endsWith('_curse') ? 'red' : 'gray', extra: level === 1 ? [] : [' ', { translate: `enchantment.level.${level}` }] }} />
})}
</>
}
const EQUIPMENT_GROUPS = [
'any',
'mainhand',
'offhand',
'hand',
'feet',
'legs',
'chest',
'head',
'armor',
'body',
]
const MODIFIER_OPERATIONS = [
'add_value',
'add_multiplied_base',
'add_multiplied_total',
]
const NEGATIVE_ATTRIBUTES = new Set([
'minecraft:burning_time',
'minecraft:fall_damage_multiplier',
])
const NEUTRAL_ATTRIBUTES = new Set([
'minecraft:gravity',
'minecraft:scale',
])
function AttributeModifiersTooltip({ data }: { data: NbtTag | undefined }) {
const modifiers = data?.isList() ? data : data?.isCompound() ? data.getList('modifiers') : new NbtList()
return <>
{EQUIPMENT_GROUPS.map(group => {
let first = true
return modifiers.map((e) => {
if (!e.isCompound()) return
const display = e.getCompound('display').getString('type')
if (display == 'hidden') return
const slot = e.has('slot') ? e.getString('slot') : 'any'
if (slot !== group) return
const wasFirst = first
first = false
let amount = e.getNumber('amount')
const type = Identifier.parse(e.getString('type'))
const id = Identifier.parse(e.getString('id'))
const operation = MODIFIER_OPERATIONS.indexOf(e.getString('operation'))
let absolute = false
if (id.equals(Identifier.create('base_attack_damage'))) {
amount += 1
absolute = true
} else if (id.equals(Identifier.create('base_attack_speed'))) {
amount += 4
absolute = true
}
if (operation !== 0) {
amount *= 100
} else if (type.equals(Identifier.create('knockback_resistance'))) {
amount *= 10
}
return <>
{wasFirst && <>
<TextComponent component={''} />
<TextComponent component={{ translate: `item.modifiers.${group}`, color: 'gray' }} />
</>}
{absolute ? (
<TextComponent component={[' ', { translate: `attribute.modifier.equals.${operation}`, with: [+amount.toFixed(2), { translate: `attribute.name.${type.path}`}], color: 'dark_green' }]} />
) : amount > 0 ? (
<TextComponent component={{ translate: `attribute.modifier.plus.${operation}`, with: [+amount.toFixed(2), { translate: `attribute.name.${type.path}`}], color: NEGATIVE_ATTRIBUTES.has(type.toString()) ? 'red' : NEUTRAL_ATTRIBUTES.has(type.toString()) ? 'gray' : 'blue' }} />
) : amount < 0 ? (
<TextComponent component={{ translate: `attribute.modifier.take.${operation}`, with: [+(-amount).toFixed(2), { translate: `attribute.name.${type.path}`}], color: NEGATIVE_ATTRIBUTES.has(type.toString()) ? 'blue' : NEUTRAL_ATTRIBUTES.has(type.toString()) ? 'gray' : 'red'}} />
) : <></>}
</>
})
})}
</>
}

View File

@@ -0,0 +1,137 @@
import type { ItemStack } from 'deepslate-1.20.4/core'
import { AttributeModifierOperation, Enchantment, Identifier, MobEffectInstance, Potion } from 'deepslate-1.20.4/core'
import { NbtList, NbtType } from 'deepslate-1.20.4/nbt'
import { useLocale } from '../contexts/Locale.jsx'
import { useVersion } from '../contexts/Version.jsx'
import { useAsync } from '../hooks/useAsync.js'
import { getLanguage, getTranslation } from '../services/Resources.js'
import { intToDisplayHexRgb, message } from '../Utils.js'
import { TextComponent } from './TextComponent.jsx'
interface Props {
item: ItemStack,
advanced?: boolean,
}
export function ItemTooltip1204({ item, advanced }: Props) {
const { version } = useVersion()
const { lang } = useLocale()
const { value: language } = useAsync(() => getLanguage(version, lang), [version, lang])
const isPotion = item.is('potion') || item.is('splash_potion') || item.is('lingering_potion')
let displayName = item.tag.getCompound('display').getString('Name')
let name: string | undefined
if (displayName) {
try {
name = JSON.parse(displayName)
} catch (e) {
console.warn(`Error parsing display name '${displayName}': ${message(e)}`)
displayName = ''
}
}
if (name === undefined) {
if (language) {
let descriptionId = `${item.id.namespace}.${item.id.path}`
if (isPotion) {
descriptionId = `${descriptionId}.effect.${Potion.fromNbt(item).name}`
}
name = getTranslation(language, `item.${descriptionId}`)
name ??= getTranslation(language, `block.${descriptionId}`)
}
name ??= item.id.path
.replace(/[_\/]/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
const lore: any[] = []
item.tag.getCompound('display').getList('Lore', NbtType.String).forEach((line) => {
try {
lore.push(JSON.parse(line['value']))
} catch (e) {
console.warn(`Error parsing lore line '${line}': ${message(e)}`)
}
})
const durability = item.getItem().durability
const enchantments = (item.is('enchanted_book') ? item.tag.getList('StoredEnchantments', NbtType.Compound) : item.tag.getList('Enchantments', NbtType.Compound)) ?? NbtList.create()
const effects = isPotion ? Potion.getAllEffects(item) : []
const attributeModifiers = isPotion ? Potion.getAllAttributeModifiers(item) : []
return <>
<TextComponent component={name} base={{ color: 'white', italic: displayName.length > 0 }} />
{shouldShow(item, 'additional') && <>
{(!advanced && displayName.length === 0 && item.is('filled_map') && item.tag.hasNumber('map')) && <>
<TextComponent component={{ text: `#${item.tag.getNumber('map')}`, color: 'gray' }} />
</>}
{(item.is('filled_map') && advanced) && <>
<TextComponent component={{ translate: 'filled_map.unknown', color: 'gray' }} />
</>}
{isPotion && effects.length === 0
? <TextComponent component={{ translate: 'effect.none', color: 'gray' }} />
: effects.map(e => {
const color = e.effect.category === 'harmful' ? 'red' : 'blue'
let component: any = { translate: `effect.${e.effect.id.namespace}.${e.effect.id.path}` }
if (e.amplifier > 0) {
component = { translate: 'potion.withAmplifier', with: [component, { translate: `potion.potency.${e.amplifier}` }] }
}
if (e.duration > 20) {
component = { translate: 'potion.withDuration', with: [component, MobEffectInstance.formatDuration(e)] }
}
return <TextComponent component={{ ...component, color }} />
})}
{attributeModifiers.length > 0 && <>
<TextComponent component='' />
<TextComponent component={{ translate: 'potion.whenDrank', color: 'dark_purple' }} />
{attributeModifiers.map(([attr, { amount, operation }]) => {
const a = operation === AttributeModifierOperation.addition ? amount * 100 : amount
if (amount > 0) {
return <TextComponent component={{ translate: `attribute.modifier.plus.${operation}`, with: [Math.floor(a * 100) / 100, { translate: `attribute.name.${attr.id.path}` }], color: 'blue' }} />
} else if (amount < 0) {
return <TextComponent component={{ translate: `attribute.modifier.take.${operation}`, with: [Math.floor(a * -100) / 100, { translate: `attribute.name.${attr.id.path}` }], color: 'red' }} />
}
return null
})}
</>}
</>}
{shouldShow(item, 'enchantments') && enchantments.map(enchantment => {
const id = enchantment.getString('id')
const lvl = enchantment.getNumber('lvl')
const ench = Enchantment.REGISTRY.get(Identifier.parse(id))
const component: any[] = [{ translate: `enchantment.${id.replace(':', '.')}`, color: ench?.isCurse ? 'red' : 'gray' }]
if (lvl !== 1 || ench?.maxLevel !== 1) {
component.push(' ', { translate: `enchantment.level.${lvl}`})
}
return <TextComponent component={component} />
})}
{item.tag.hasCompound('display') && <>
{shouldShow(item, 'dye') && item.tag.getCompound('display').hasNumber('color') && (advanced
? <TextComponent component={{ translate: 'item.color', with: [intToDisplayHexRgb(item.tag.getCompound('display').getNumber('color'))], color: 'gray' }} />
: <TextComponent component={{ translate: 'item.dyed', color: 'gray' }} />)}
{lore.map((component) => <TextComponent component={component} base={{ color: 'dark_purple', italic: true }} />)}
</>}
{shouldShow(item, 'unbreakable') && item.tag.getBoolean('Unbreakable') && <TextComponent component={{ translate: 'item.unbreakable', color: 'blue' }} />}
{(advanced && item.tag.getNumber('Damage') > 0 && durability) && <TextComponent component={{ translate: 'item.durability', with: [`${durability - item.tag.getNumber('Damage')}`, `${durability}`] }} />}
{advanced && <>
<TextComponent component={{ text: item.id.toString(), color: 'dark_gray'}} />
{item.tag.size > 0 && <TextComponent component={{ translate: 'item.nbt_tags', with: [item.tag.size], color: 'dark_gray' }} />}
</>}
</>
}
const TooltipMasks = {
enchantments: 1,
modifiers: 2,
unbreakable: 4,
can_destroy: 8,
can_place: 16,
additional: 32,
dye: 64,
upgrades: 128,
}
function shouldShow(item: ItemStack, mask: keyof typeof TooltipMasks) {
const flags = item.tag.getNumber('HideFlags')
return (flags & TooltipMasks[mask]) === 0
}

View File

@@ -1,21 +1,22 @@
import type { JSX } from 'preact'
import { useCallback, useEffect } from 'preact/hooks'
import { useModal } from '../contexts/Modal.jsx'
import { LOSE_FOCUS } from '../hooks/index.js'
const MODALS_KEY = 'data-modals'
interface Props extends JSX.HTMLAttributes<HTMLDivElement> {
onDismiss: () => void,
}
interface Props extends JSX.HTMLAttributes<HTMLDivElement> {}
export function Modal(props: Props) {
const { hideModal } = useModal()
useEffect(() => {
addCurrentModals(1)
window.addEventListener('click', props.onDismiss)
window.addEventListener('click', hideModal)
return () => {
addCurrentModals(-1)
window.removeEventListener('click', props.onDismiss)
window.removeEventListener('click', hideModal)
}
})
}, [hideModal])
const onClick = useCallback((e: MouseEvent) => {
e.stopPropagation()

View File

@@ -10,9 +10,9 @@ export const Octicon = {
chevron_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"></path></svg>,
chevron_up: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.22 9.78a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 0l4.25 4.25a.75.75 0 01-1.06 1.06L8 6.06 4.28 9.78a.75.75 0 01-1.06 0z"></path></svg>,
circle_slash: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM3.965 13.096a6.5 6.5 0 0 0 9.131-9.131ZM1.5 8a6.474 6.474 0 0 0 1.404 4.035l9.131-9.131A6.499 6.499 0 0 0 1.5 8Z"></path></svg>,
clippy: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path></svg>,
code: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>,
codescan_checkmark: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M10.28 6.28a.75.75 0 10-1.06-1.06L6.25 8.19l-.97-.97a.75.75 0 00-1.06 1.06l1.5 1.5a.75.75 0 001.06 0l3.5-3.5z"></path><path fill-rule="evenodd" d="M7.5 15a7.469 7.469 0 004.746-1.693l2.474 2.473a.75.75 0 101.06-1.06l-2.473-2.474A7.5 7.5 0 107.5 15zm0-13.5a6 6 0 104.094 10.386.75.75 0 01.293-.292A6 6 0 007.5 1.5z"></path></svg>,
copy: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>,
diff_added: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1Zm10.5 1.5H2.75a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM8 4a.75.75 0 0 1 .75.75v2.5h2.5a.75.75 0 0 1 0 1.5h-2.5v2.5a.75.75 0 0 1-1.5 0v-2.5h-2.5a.75.75 0 0 1 0-1.5h2.5v-2.5A.75.75 0 0 1 8 4Z"></path></svg>,
diff_modified: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M13.25 1c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM8 10a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 10Z"></path></svg>,
diff_removed: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M13.25 1c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1ZM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25Zm8.5 6.25h-6.5a.75.75 0 0 1 0-1.5h6.5a.75.75 0 0 1 0 1.5Z"></path></svg>,
@@ -44,6 +44,7 @@ export const Octicon = {
moon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786zm1.616 1.945a7 7 0 01-7.678 7.678 5.5 5.5 0 107.678-7.678z"></path></svg>,
mortar_board: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M7.693 1.066a.747.747 0 0 1 .614 0l7.25 3.25a.75.75 0 0 1 0 1.368L13 6.831v2.794c0 1.024-.81 1.749-1.66 2.173-.893.447-2.075.702-3.34.702-.278 0-.55-.012-.816-.036a.75.75 0 0 1 .133-1.494c.22.02.45.03.683.03 1.082 0 2.025-.221 2.67-.543.69-.345.83-.682.83-.832V7.503L8.307 8.934a.747.747 0 0 1-.614 0L4 7.28v1.663c.296.105.575.275.812.512.438.438.688 1.059.688 1.796v3a.75.75 0 0 1-.75.75h-3a.75.75 0 0 1-.75-.75v-3c0-.737.25-1.358.688-1.796.237-.237.516-.407.812-.512V6.606L.443 5.684a.75.75 0 0 1 0-1.368ZM2.583 5 8 7.428 13.416 5 8 2.572ZM2.5 11.25v2.25H4v-2.25c0-.388-.125-.611-.25-.735a.697.697 0 0 0-.5-.203.707.707 0 0 0-.5.203c-.125.124-.25.347-.25.735Z"></path></svg>,
package: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>,
paste: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M3.626 3.533a.249.249 0 0 0-.126.217v9.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-9.5a.249.249 0 0 0-.126-.217.75.75 0 0 1 .752-1.298c.541.313.874.89.874 1.515v9.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-9.5c0-.625.333-1.202.874-1.515a.75.75 0 0 1 .752 1.298ZM5.75 1h4.5a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 4.75v-3A.75.75 0 0 1 5.75 1Zm.75 3h3V2.5h-3Z"></path></svg>,
pencil: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path></svg>,
play: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>,
plus: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2a.75.75 0 01.75.75v4.5h4.5a.75.75 0 010 1.5h-4.5v4.5a.75.75 0 01-1.5 0v-4.5h-4.5a.75.75 0 010-1.5h4.5v-4.5A.75.75 0 018 2z"></path></svg>,

View File

@@ -1,4 +1,5 @@
import { useMemo } from 'preact/hooks'
import { useLocale } from '../contexts/Locale.jsx'
import { useVersion } from '../contexts/Version.jsx'
import { useAsync } from '../hooks/useAsync.js'
import { getLanguage, replaceTranslation } from '../services/Resources.js'
@@ -21,10 +22,11 @@ interface PartData extends StyleData {
interface Props {
component: unknown,
base?: StyleData,
shadow?: boolean,
oneline?: boolean,
}
export function TextComponent({ component, base = { color: 'white' }, shadow = true }: Props) {
export function TextComponent({ component, base = { color: 'white' }, oneline }: Props) {
const { version } = useVersion()
const { lang } = useLocale()
const state = JSON.stringify(component)
const parts = useMemo(() => {
@@ -33,15 +35,10 @@ export function TextComponent({ component, base = { color: 'white' }, shadow = t
return parts
}, [state, base])
const { value: language } = useAsync(() => getLanguage(version), [version])
const { value: language } = useAsync(() => getLanguage(version, lang), [version, lang])
return <div class="text-component">
{shadow && <div>
{parts.map(p => <TextPart part={p} shadow={true} lang={language ?? {}} />)}
</div>}
<div class="text-foreground">
{parts.map(p => <TextPart part={p} lang={language ?? {}} />)}
</div>
{parts.map(p => <TextPart part={p} lang={language ?? {}} oneline={oneline} />)}
</div>
}
@@ -76,18 +73,18 @@ function visitComponent(component: unknown, consumer: (c: PartData) => void) {
}
}
function inherit(component: object, base: PartData) {
function inherit(component: any, base: PartData) {
return {
color: base.color,
bold: base.bold,
italic: base.italic,
underlined: base.underlined,
strikethrough: base.strikethrough,
...component,
color: component.color ?? base.color,
bold: component.bold ?? base.bold,
italic: component.italic ?? base.italic,
underlined: component.underlined ?? base.underlined,
strikethrough: component.strikethrough ?? base.strikethrough,
}
}
const TextColors = {
const TextColors: Record<string, [string, string]> = {
black: ['#000', '#000'],
dark_blue: ['#00A', '#00002A'],
dark_green: ['#0A0', '#002A00'],
@@ -106,19 +103,16 @@ const TextColors = {
white: ['#FFF', '#3F3F3F'],
}
type TextColorKey = keyof typeof TextColors
const TextColorKeys = Object.keys(TextColors)
function TextPart({ part, shadow, lang }: { part: PartData, shadow?: boolean, lang: Record<string, string> }) {
if (part.translate) {
const str = resolveTranslate(part.translate, part.fallback, part.with, lang)
return <span style={createStyle(part, shadow)}>{str}</span>
}
return <span style={createStyle(part, shadow)}>{part.text}</span>
function TextPart({ part, lang, oneline }: { part: PartData, lang: Record<string, string>, oneline?: boolean }) {
let text = part.translate
? resolveTranslate(part.translate, part.fallback, part.with, lang)
: (part.text ?? '')
text = oneline ? text.replaceAll('\n', '␊') : text
return <span style={createStyle(part)}>{text}</span>
}
function resolveTranslate(translate: string, fallback: string | undefined, with_: any[] | undefined, lang: Record<string, string>): string {
const str = lang[translate] ?? fallback
const str = lang[translate] ?? fallback ?? translate
if (typeof str !== 'string') return translate
const params = with_?.map((c): string => {
if (typeof c === 'string' || typeof c === 'number') return `${c}`
@@ -129,11 +123,10 @@ function resolveTranslate(translate: string, fallback: string | undefined, with_
return replaceTranslation(str, params)
}
function createStyle(style: StyleData, shadow?: boolean) {
function createStyle(style: StyleData) {
return {
color: style.color && (TextColorKeys.includes(style.color)
? TextColors[style.color as TextColorKey][shadow ? 1 : 0]
: shadow ? 'transparent' : style.color),
color: style.color ? (TextColors[style.color]?.[0] ?? style.color) : undefined,
'--shadow-color': style.color ? TextColors[style.color]?.[1] : undefined,
fontWeight: (style.bold === true) ? 'bold' : undefined,
fontStyle: (style.italic === true) ? 'italic' : undefined,
textDecoration: (style.underlined === true)

View File

@@ -1,8 +1,8 @@
import { useMemo, useState } from 'preact/hooks'
import config from '../Config.js'
import { Store } from '../Store.js'
import { useLocale } from '../contexts/index.js'
import type { VersionId } from '../services/index.js'
import { Store } from '../Store.js'
import { Btn } from './Btn.js'
import { BtnMenu } from './BtnMenu.js'
@@ -29,7 +29,7 @@ export function VersionSwitcher({ value, allowed, hasAny, onChange, onAny }: Pro
const hasMoreVersions = useMemo(() => {
return versions.some(v => !(v.show || v.id === value))
}, [])
}, [versions, value])
const shownVersions = useMemo(() => {
return versions.filter(v => v.show || v.id === value || showMore)

View File

@@ -1,7 +1,8 @@
import { Identifier } from 'deepslate'
import { deepClone, deepEqual } from '../../Utils.js'
import type { BlockStateData } from '../../services/DataFetcher.js'
import { fetchAllPresets, fetchBlockStates } from '../../services/DataFetcher.js'
import type { VersionId } from '../../services/Schemas.js'
import type { VersionId } from '../../services/Versions.js'
import { deepClone, deepEqual } from '../../Utils.js'
import type { CustomizedOreModel } from './CustomizedModel.js'
import { CustomizedModel } from './CustomizedModel.js'
@@ -17,7 +18,7 @@ interface Context {
model: CustomizedModel,
initial: CustomizedModel,
version: VersionId,
blockStates: Map<string, {properties: Record<string, string[]>, default: Record<string, string>}>,
blockStates: Map<string, BlockStateData>,
vanilla: CustomizedPack,
out: CustomizedPack,
featureCollisionIndex: number,
@@ -77,7 +78,7 @@ function generateNoiseSettings(ctx: Context) {
sea_level: ctx.model.seaLevel,
default_fluid: {
Name: defaultFluid,
Properties: ctx.blockStates.get(defaultFluid)?.default,
Properties: ctx.blockStates.get(defaultFluid.replace(/^minecraft:/, ''))?.[1],
},
noise: {
...vanilla.noise,

View File

@@ -1,4 +1,4 @@
import type { VersionId } from '../../services/Schemas.js'
import type { VersionId } from '../../services/Versions.js'
export interface CustomizedOreModel {
size: number,

View File

@@ -26,7 +26,7 @@ export function CustomizedOre({ model, value, initial, onChange }: Props) {
value={calcHeight(model, value.minAboveBottom, value.minBelowTop, value.minHeight) ?? 0}
onChange={v => changeOre(value.minAboveBottom !== undefined ? { minAboveBottom: v - model.minHeight } : value.minBelowTop != undefined ? { minBelowTop: model.maxHeight - v } : { minHeight: v })}
min={-64} max={320} initial={calcHeight(model, initial.minAboveBottom, initial.minBelowTop, initial.minHeight) ?? 0} />
<CustomizedSlider label={value.trapezoid ? 'Max triangle' : 'Max height'} help="The heighest Y level the ore vein can generate at"
<CustomizedSlider label={value.trapezoid ? 'Max triangle' : 'Max height'} help="The highest Y level the ore vein can generate at"
value={calcHeight(model, value.maxAboveBottom, value.maxBelowTop, value.maxHeight) ?? 0}
onChange={v => changeOre(value.maxAboveBottom !== undefined ? { maxAboveBottom: v - model.minHeight } : value.maxBelowTop != undefined ? { maxBelowTop: model.maxHeight - v } : { maxHeight: v })}
min={-64} max={320} initial={calcHeight(model, initial.maxAboveBottom, initial.maxBelowTop, initial.maxHeight) ?? 0} />

View File

@@ -1,8 +1,8 @@
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import config from '../../Config.js'
import { deepClone, deepEqual, writeZip } from '../../Utils.js'
import { useVersion } from '../../contexts/Version.jsx'
import { stringifySource } from '../../services/Source.js'
import { deepClone, deepEqual, writeZip } from '../../Utils.js'
import { Btn } from '../Btn.jsx'
import { ErrorPanel } from '../ErrorPanel.jsx'
import { Octicon } from '../Octicon.jsx'
@@ -44,11 +44,13 @@ export function CustomizedPanel({ tab }: Props) {
const entries = Object.entries(pack).flatMap(([type, files]) => {
const prefix = `data/minecraft/${type}/`
return [...files.entries()].map(([name, data]) => {
return [prefix + name + '.json', stringifySource(data, 'json')] as [string, string]
const text = stringifySource(JSON.stringify(data, null, 2), 'json')
return [prefix + name + '.json', new TextEncoder().encode(text)] as [string, Uint8Array]
})
})
const pack_format = config.versions.find(v => v.id === version)!.pack_format
entries.push(['pack.mcmeta', stringifySource({ pack: { pack_format, description: 'Customized world from misode.github.io' } }, 'json')])
const packMcmetaText = stringifySource(JSON.stringify({ pack: { pack_format, description: 'Customized world from misode.github.io' } }, null, 2), 'json')
entries.push(['pack.mcmeta', new TextEncoder().encode(packMcmetaText)])
const url = await writeZip(entries)
download.current.setAttribute('href', url)
download.current.setAttribute('download', 'customized.zip')

View File

@@ -1,4 +1,3 @@
import type { NodeChildren } from '@mcschema/core'
import { NumberInput, RangeInput } from '../index.js'
import { CustomizedInput } from './CustomizedInput.jsx'
@@ -12,7 +11,6 @@ interface Props {
initial?: number,
error?: string,
onChange: (value: number) => void,
children?: NodeChildren,
}
export function CustomizedSlider(props: Props) {
const isInteger = (props.step ?? 1) >= 1

View File

@@ -1,24 +0,0 @@
import { useMemo, useState } from 'preact/hooks'
import { Btn, BtnInput } from '../index.js'
interface Props {
values?: string[],
onSelect?: (value: string) => unknown,
searchPlaceholder?: string,
noResults?: string,
}
export function SearchList({ values, onSelect, searchPlaceholder, noResults }: Props) {
const [search, setSearch] = useState('')
const results = useMemo(() => {
const terms = search.trim().split(' ')
return values?.filter(v => terms.every(t => v.includes(t))) ?? []
}, [values, search])
return <>
<BtnInput icon="search" large value={search} onChange={setSearch} doSelect={1} placeholder={searchPlaceholder ?? 'Search'} />
<div class="result-list">
{results.map(v => <Btn key={v} label={v} onClick={() => onSelect?.(v)} />)}
{results.length === 0 && <Btn label={noResults ?? 'No results'}/>}
</div>
</>
}

View File

@@ -1,3 +1,2 @@
export * from './Checkbox.js'
export * from './Input.js'
export * from './SearchList.js'

View File

@@ -1,31 +1,64 @@
import { DataModel } from '@mcschema/core'
import { useState } from 'preact/hooks'
import type { DocAndNode } from '@spyglassmc/core'
import { Identifier } from 'deepslate'
import { useCallback, useState } from 'preact/hooks'
import type { Method } from '../../Analytics.js'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
import type { ConfigGenerator } from '../../Config.js'
import { getProjectRoot, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { useModal } from '../../contexts/Modal.jsx'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import { genPath, message } from '../../Utils.js'
import { Btn } from '../Btn.js'
import { TextInput } from '../forms/index.js'
import { Modal } from '../Modal.js'
interface Props {
model: DataModel,
id: string,
method: string,
onClose: () => void,
docAndNode: DocAndNode,
gen: ConfigGenerator,
method: Method,
}
export function FileCreation({ model, id, method, onClose }: Props) {
export function FileCreation({ docAndNode, gen, method }: Props) {
const { locale } = useLocale()
const { projects, project, updateFile } = useProject()
const [fileId, setFileId] = useState(id === 'pack_mcmeta' ? 'pack' : '')
const { version } = useVersion()
const { hideModal } = useModal()
const { project } = useProject()
const { client } = useSpyglass()
const doSave = () => {
Analytics.saveProjectFile(id, projects.length, project.files.length, method as any)
updateFile(id, undefined, { type: id, id: fileId, data: DataModel.unwrapLists(model.data) })
onClose()
const [fileId, setFileId] = useState(gen.id === 'pack_mcmeta' ? 'pack' : '')
const [error, setError] = useState<string>()
const changeFileId = (str: string) => {
setError(undefined)
setFileId(str)
}
return <Modal class="file-modal" onDismiss={onClose}>
const doSave = useCallback(() => {
if (!project) {
return
}
if (!fileId.match(/^([a-z0-9_.-]+:)?[a-z0-9/_.-]+$/)) {
setError('Invalid resource location')
return
}
const id = Identifier.parse(fileId.includes(':') || project.namespace === undefined ? fileId : `${project.namespace}:${fileId}`)
const pack = gen.tags?.includes('assets') ? 'assets' : 'data'
const projectRoot = getProjectRoot(project)
const uri = gen.id === 'pack_mcmeta'
? `${projectRoot}pack.mcmeta`
: `${projectRoot}${pack}/${id.namespace}/${genPath(gen, version)}/${id.path}${gen.ext ?? '.json'}`
Analytics.saveProjectFile(method)
const text = docAndNode.doc.getText()
client.fs.writeFile(uri, text).then(() => {
hideModal()
}).catch((e) => {
setError(message(e))
})
}, [version, project, client, fileId ])
return <Modal class="file-modal">
<p>{locale('project.save_current_file')}</p>
<TextInput autofocus={id !== 'pack_mcmeta'} class="btn btn-input" value={fileId} onChange={setFileId} onEnter={doSave} onCancel={onClose} placeholder={locale('resource_location')} spellcheck={false} readOnly={id === 'pack_mcmeta'} />
<TextInput autofocus={gen.id !== 'pack_mcmeta'} class="btn btn-input" value={fileId} onChange={changeFileId} onEnter={doSave} onCancel={hideModal} placeholder={locale('resource_location')} spellcheck={false} readOnly={gen.id === 'pack_mcmeta'} />
{error !== undefined && <span class="invalid">{error}</span>}
<Btn icon="file" label={locale('project.save')} onClick={doSave} />
</Modal>
}

View File

@@ -1,29 +1,40 @@
import { useState } from 'preact/hooks'
import { useCallback, useState } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { useLocale } from '../../contexts/index.js'
import { useModal } from '../../contexts/Modal.jsx'
import { Btn } from '../Btn.js'
import { TextInput } from '../forms/index.js'
import { Modal } from '../Modal.js'
interface Props {
id: string,
name: string,
onClose: () => void,
oldId: string,
onRename: (newId: string) => void,
}
export function FileRenaming({ id, name, onClose }: Props) {
export function FileRenaming({ oldId, onRename }: Props) {
const { locale } = useLocale()
const { projects, project, updateFile } = useProject()
const [fileId, setFileId] = useState(name)
const { hideModal } = useModal()
const [fileId, setFileId] = useState(oldId)
const [error, setError] = useState<string>()
const doSave = () => {
Analytics.renameProjectFile(id, projects.length, project.files.length, 'menu')
updateFile(id, name, { type: id, id: fileId })
onClose()
}
const changeFileId = useCallback((str: string) => {
setError(undefined)
setFileId(str)
}, [])
return <Modal class="file-modal" onDismiss={onClose}>
const doRename = useCallback(() => {
if (!fileId.match(/^([a-z0-9_.-]+:)?[a-z0-9/_.-]+$/)) {
setError('Invalid resource location')
return
}
Analytics.renameProjectFile('menu')
onRename(fileId)
hideModal()
}, [fileId, hideModal])
return <Modal class="file-modal">
<p>{locale('project.rename_file')}</p>
<TextInput autofocus class="btn btn-input" value={fileId} onChange={setFileId} onEnter={doSave} placeholder={locale('resource_location')} spellcheck={false} />
<Btn icon="pencil" label={locale('project.rename')} onClick={doSave} />
<TextInput autofocus class="btn btn-input" value={fileId} onChange={changeFileId} onEnter={doRename} onCancel={hideModal} placeholder={locale('resource_location')} spellcheck={false} />
{error !== undefined && <span class="invalid">{error}</span>}
<Btn icon="pencil" label={locale('project.rename')} onClick={doRename} />
</Modal>
}

View File

@@ -0,0 +1,41 @@
import type { DocAndNode } from '@spyglassmc/core'
import { JsonFileNode } from '@spyglassmc/json'
import { useErrorBoundary } from 'preact/hooks'
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
import { message } from '../../Utils.js'
import { ErrorPanel } from '../ErrorPanel.jsx'
import { JsonFileView } from './JsonFileView.jsx'
type FileViewProps = {
docAndNode: DocAndNode | undefined,
}
export function FileView({ docAndNode: original }: FileViewProps) {
const { serviceLoading } = useSpyglass()
const [error, errorRetry] = useErrorBoundary()
if (error) {
const viewError = new Error(`Error viewing the file: ${message(error)}`)
if (error.stack) {
viewError.stack = error.stack
}
return <ErrorPanel error={viewError} onDismiss={errorRetry} />
}
const docAndNode = useDocAndNode(original)
if (!docAndNode || serviceLoading) {
return <div class="file-view flex flex-col gap-1">
<div class="skeleton rounded-md h-[34px] w-[200px]"></div>
<div class="skeleton rounded-md h-[34px] w-[240px]"></div>
<div class="skeleton rounded-md h-[34px] w-[190px] ml-[18px]"></div>
<div class="skeleton rounded-md h-[34px] w-[130px] ml-[18px]"></div>
<div class="skeleton rounded-md h-[34px] w-[290px]"></div>
</div>
}
const fileNode = docAndNode?.node.children[0]
if (JsonFileNode.is(fileNode)) {
return <JsonFileView docAndNode={docAndNode} node={fileNode.children[0]} />
}
return <ErrorPanel error={`Cannot view file ${docAndNode.doc.uri}`} />
}

View File

@@ -1,10 +1,10 @@
import { useMemo } from 'preact/hooks'
import type { ConfigGenerator } from '../../Config.js'
import config from '../../Config.js'
import { cleanUrl } from '../../Utils.js'
import { useLocale } from '../../contexts/Locale.jsx'
import type { VersionId } from '../../services/Schemas.js'
import { checkVersion } from '../../services/Schemas.js'
import type { VersionId } from '../../services/Versions.js'
import { checkVersion } from '../../services/Versions.js'
import { cleanUrl } from '../../Utils.js'
import { Badge, Card, Icons, ToolCard } from '../index.js'
const VERSION_SEP = ' • '
@@ -24,7 +24,7 @@ export function GeneratorCard({ id, minimal }: Props) {
return gen
}, [id])
const title = locale(gen.partner ? `partner.${gen.partner}.${gen.id}` : gen.id)
const title = locale(`generator.${gen.id}`)
const icon = Object.keys(Icons).includes(id) ? id as keyof typeof Icons : undefined
@@ -40,12 +40,12 @@ export function GeneratorCard({ id, minimal }: Props) {
}, [gen])
const versionText = useMemo(() => {
if (versions.length <= 5) {
if (versions.length <= 3) {
return versions.join(VERSION_SEP)
}
return versions[0] + VERSION_SEP
+ '...' + VERSION_SEP
+ versions.slice(-3).join(VERSION_SEP)
+ versions.slice(-2).join(VERSION_SEP)
}, [versions])
const tags = useMemo(() => {
@@ -53,7 +53,7 @@ export function GeneratorCard({ id, minimal }: Props) {
return []
}, [gen])
return <Card title={<>{title}{icon && Icons[icon]}</>} overlay={gen.partner ? locale(`partner.${gen.partner}`) : versionText} link={cleanUrl(gen.url)}>
return <Card title={<>{title}{icon && Icons[icon]}</>} overlay={gen.dependency ? locale(`partner.${gen.dependency}`) : versionText} link={cleanUrl(gen.url)}>
{!gen.noPath && <p class="card-subtitle">/{gen.path ?? gen.id}</p>}
{tags.length > 0 && <div class="badges-list">
{tags.sort().map(tag => <Badge label={tag} />)}

View File

@@ -2,7 +2,7 @@ import { useMemo, useState } from 'preact/hooks'
import type { ConfigGenerator } from '../../Config.js'
import config from '../../Config.js'
import { useLocale, useVersion } from '../../contexts/index.js'
import { checkVersion } from '../../services/Schemas.js'
import { checkVersion } from '../../services/Versions.js'
import { GeneratorCard, TextInput, VersionSwitcher } from '../index.js'
interface Props {
@@ -26,16 +26,9 @@ export function GeneratorList({ predicate }: Props) {
}, [version, versionFilter])
const filteredGenerators = useMemo(() => {
const query = search.split(' ').map(q => q.trim().toLowerCase()).filter(q => q.length > 0)
return versionedGenerators.filter(gen => {
const content = `${gen.id} ${gen.tags?.join(' ') ?? ''} ${gen.path ?? ''} ${gen.partner ?? ''} ${locale(gen.id).toLowerCase()}`
return query.every(q => {
if (q.startsWith('!')) {
return q.length === 1 || !content.includes(q.slice(1))
}
return content.includes(q)
})
})
const results = versionedGenerators
.map(g => ({ ...g, name: locale(`generator.${g.id}`).toLowerCase() }))
return searchGenerators(results, search)
}, [versionedGenerators, search, locale])
return <div class="generator-list">
@@ -45,10 +38,25 @@ export function GeneratorList({ predicate }: Props) {
</div>
{filteredGenerators.length === 0 ? <>
<span class="note">{locale('generators.no_results')}</span>
</> : <div class="card-column">
</> : <div class="card-grid">
{filteredGenerators.map(gen =>
<GeneratorCard id={gen.id} />
)}
</div>}
</div>
}
export function searchGenerators(generators: (ConfigGenerator & { name: string})[], search?: string) {
if (search) {
const parts = search.split(' ').map(q => q.trim().toLowerCase()).filter(q => q.length > 0)
generators = generators.filter(g => parts.some(p => g.name.includes(p))
|| parts.some(p => g.path?.includes(p) ?? false)
|| parts.some(p => g.tags?.some(t => t.includes(p)) ?? false)
|| parts.some(p => g.aliases?.some(a => a.includes(p)) ?? false))
}
generators.sort((a, b) => a.name.localeCompare(b.name))
if (search) {
generators.sort((a, b) => (b.name.startsWith(search) ? 1 : 0) - (a.name.startsWith(search) ? 1 : 0))
}
return generators
}

View File

@@ -0,0 +1,79 @@
import type { DocAndNode, Range } from '@spyglassmc/core'
import { dissectUri } from '@spyglassmc/java-edition/lib/binder/index.js'
import type { JsonNode } from '@spyglassmc/json'
import { JsonFileNode } from '@spyglassmc/json'
import { useCallback, useMemo } from 'preact/hooks'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import { getRootType, simplifyType } from './McdocHelpers.js'
import type { McdocContext } from './McdocRenderer.jsx'
import { McdocRoot } from './McdocRenderer.jsx'
type JsonFileViewProps = {
docAndNode: DocAndNode,
node: JsonNode,
}
export function JsonFileView({ docAndNode, node }: JsonFileViewProps) {
const { service } = useSpyglass()
const makeEdit = useCallback((edit: (range: Range) => JsonNode | undefined) => {
if (!service) {
return
}
service.applyEdit(docAndNode.doc.uri, (fileNode) => {
const jsonFile = fileNode.children[0]
if (JsonFileNode.is(jsonFile)) {
const original = jsonFile.children[0]
const newNode = edit(original.range)
if (newNode !== undefined) {
newNode.parent = fileNode
fileNode.children[0] = newNode
}
}
})
}, [service, docAndNode])
const ctx = useMemo<McdocContext | undefined>(() => {
if (!service) {
return undefined
}
const errors = [
...docAndNode.node.binderErrors ?? [],
...docAndNode.node.checkerErrors ?? [],
...docAndNode.node.linterErrors ?? [],
]
const checkerCtx = service.getCheckerContext(docAndNode.doc, errors)
return { ...checkerCtx, makeEdit }
}, [docAndNode, service, makeEdit])
const resourceType = useMemo(() => {
if (docAndNode.doc.uri.endsWith('/pack.mcmeta')) {
return 'pack_mcmeta'
}
if (ctx === undefined) {
return undefined
}
const res = dissectUri(docAndNode.doc.uri, ctx)
return res?.category
}, [docAndNode, ctx])
const mcdocType = useMemo(() => {
if (!ctx || !resourceType) {
return undefined
}
const rootType = getRootType(resourceType)
const type = simplifyType(rootType, ctx)
return type
}, [resourceType, ctx])
return <div class="file-view node-root" data-category={getCategory(resourceType)}>
{(ctx && mcdocType) && <McdocRoot type={mcdocType} node={node} ctx={ctx} />}
</div>
}
function getCategory(type: string | undefined) {
switch (type) {
case 'item_modifier': return 'function'
case 'predicate': return 'predicate'
default: return undefined
}
}

View File

@@ -0,0 +1,493 @@
import * as core from '@spyglassmc/core'
import type { JsonNode, JsonPairNode } from '@spyglassmc/json'
import { JsonArrayNode, JsonObjectNode, JsonStringNode } from '@spyglassmc/json'
import { JsonStringOptions } from '@spyglassmc/json/lib/parser/string.js'
import type { Attributes, AttributeValue, ListType, McdocType, NumericType, PrimitiveArrayType, TupleType, UnionType } from '@spyglassmc/mcdoc'
import { NumericRange, RangeKind } from '@spyglassmc/mcdoc'
import type { McdocCheckerContext, SimplifiedMcdocType, SimplifiedMcdocTypeNoUnion, SimplifyValueNode } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js'
import { simplify } from '@spyglassmc/mcdoc/lib/runtime/checker/index.js'
import config from '../../Config.js'
import { randomInt, randomSeed } from '../../Utils.js'
export function getRootType(id: string): McdocType {
if (id === 'pack_mcmeta') {
return { kind: 'reference', path: '::java::pack::Pack' }
}
if (id === 'text_component' ) {
return { kind: 'reference', path: '::java::util::text::Text' }
}
if (id.startsWith('tag/')) {
const attribute: AttributeValue = {
kind: 'tree',
values: {
registry: { kind: 'literal', value: { kind: 'string', value: id.slice(4) } },
tags: { kind: 'literal', value: { kind: 'string', value: 'allowed' } },
},
}
return {
kind: 'concrete',
child: { kind: 'reference', path: '::java::data::tag::Tag' },
typeArgs: [{ kind: 'string', attributes: [{ name: 'id', value: attribute }] }],
}
}
return {
kind: 'dispatcher',
registry: 'minecraft:resource',
parallelIndices: [{ kind: 'static', value: id }],
}
}
export function getRootDefault(id: string, ctx: core.CheckerContext) {
const type = simplifyType(getRootType(id), ctx)
return getDefault(type, core.Range.create(0), ctx)
}
export function getDefault(type: SimplifiedMcdocType, range: core.Range, ctx: core.CheckerContext): JsonNode {
if (type.kind === 'string') {
return JsonStringNode.mock(range)
}
if (type.kind === 'boolean') {
return { type: 'json:boolean', range, value: false }
}
if (isNumericType(type)) {
let num: number | bigint = 0
if (type.valueRange) {
// Best effort. First try 0 or 1, else set to the lowest bound
if (NumericRange.isInRange(type.valueRange, 0)) {
num = 0
} else if (NumericRange.isInRange(type.valueRange, 1)) {
num = 1
} else if (type.valueRange.min && type.valueRange.min > 0) {
num = type.valueRange.min
if (RangeKind.isLeftExclusive(type.valueRange.kind)) {
// Assume that left exclusive ranges are longer than 1
num += 1
}
}
}
if (type.attributes?.some(a => a.name === 'pack_format')) {
// Set to the latest pack format
const release = ctx.project['loadedVersion']
const version = config.versions.find(v => v.ref === release || v.id === release)
if (version) {
num = version.pack_format
}
}
if (type.attributes?.some(a => a.name === 'random')) {
// Generate random number
if (type.kind === 'long') {
num = randomSeed()
} else {
num = randomInt()
}
}
const value: core.LongNode | core.FloatNode = typeof num !== 'number' || Number.isInteger(num)
? { type: 'long', range, value: typeof num === 'number' ? BigInt(num) : num }
: { type: 'float', range, value: num }
return { type: 'json:number', range, value, children: [value] }
}
if (type.kind === 'struct' || type.kind === 'any' || type.kind === 'unsafe') {
const object = JsonObjectNode.mock(range)
if (type.kind === 'struct') {
for (const field of type.fields) {
if (field.kind === 'pair' && !field.optional && (typeof field.key === 'string' || field.key.kind === 'literal')) {
const key: JsonStringNode = {
type: 'json:string',
range,
options: JsonStringOptions,
value: typeof field.key === 'string' ? field.key : field.key.value.value.toString(),
valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }],
}
const value = getDefault(simplifyType(field.type, ctx), range, ctx)
const pair: JsonPairNode = {
type: 'pair',
range,
key: key,
value: value,
children: [key, value],
}
key.parent = pair
value.parent = pair
object.children.push(pair)
pair.parent = object
}
}
}
return object
}
if (isListOrArray(type)) {
const array = JsonArrayNode.mock(range)
const minLength = type.lengthRange?.min ?? 0
if (minLength > 0) {
for (let i = 0; i < minLength; i += 1) {
const child = getDefault(simplifyType(getItemType(type), ctx), range, ctx)
const itemNode: core.ItemNode<JsonNode> = {
type: 'item',
range,
children: [child],
value: child,
}
child.parent = itemNode
array.children.push(itemNode)
itemNode.parent = array
}
}
return array
}
if (type.kind === 'tuple') {
return {
type: 'json:array',
range,
children: type.items.map(item => {
const valueNode = getDefault(simplifyType(item, ctx), range, ctx)
const itemNode: core.ItemNode<JsonNode> = {
type: 'item',
range,
children: [valueNode],
value: valueNode,
}
valueNode.parent = itemNode
return itemNode
}),
}
}
if (type.kind === 'union') {
if (type.members.length === 0) {
return { type: 'json:null', range }
}
return getDefault(type.members[0], range, ctx)
}
if (type.kind === 'enum') {
return getDefault({ kind: 'literal', value: { kind: type.enumKind ?? 'string', value: type.values[0].value } as any }, range, ctx)
}
if (type.kind === 'literal') {
if (type.value.kind === 'string') {
return { type: 'json:string', range, options: JsonStringOptions, value: type.value.value, valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(range.start) }] }
}
if (type.value.kind === 'boolean') {
return { type: 'json:boolean', range, value: type.value.value }
}
const value: core.FloatNode | core.LongNode = type.value.kind === 'float' || type.value.kind === 'double'
? { type: 'float', range, value: type.value.value }
: { type: 'long', range, value: BigInt(type.value.value) }
return { type: 'json:number', range, value, children: [value] }
}
return { type: 'json:null', range }
}
export function getChange(type: SimplifiedMcdocTypeNoUnion, oldType: SimplifiedMcdocTypeNoUnion, oldNode: JsonNode, ctx: core.CheckerContext): JsonNode {
const node = getDefault(type, oldNode.range, ctx)
if (JsonArrayNode.is(node) && isListOrArray(type)) {
// From X to [X]
const newItemType = simplifyType(getItemType(type), ctx)
const possibleItemTypes = newItemType.kind === 'union' ? newItemType.members : [newItemType]
for (const possibleType of possibleItemTypes) {
if (quickEqualTypes(oldType, possibleType)) {
const newItem: core.ItemNode<JsonNode> = {
type: 'item',
range: node.range,
children: [oldNode],
value: oldNode,
parent: node,
}
oldNode.parent = newItem
node.children.splice(0, node.children.length, newItem)
return node
}
}
}
if (JsonArrayNode.is(oldNode) && isListOrArray(oldType)) {
// From [X] to X
const oldItemType = simplifyType(getItemType(oldType), ctx)
if (oldItemType.kind !== 'union' && quickEqualTypes(type, oldItemType)) {
const oldItem = oldNode.children[0]
if (oldItem?.value) {
return oldItem.value
}
}
}
if (JsonObjectNode.is(node) && type.kind === 'struct') {
// From X to {k: X}
for (const field of type.fields) {
const fieldKey = field.key
if (field.optional || fieldKey.kind !== 'literal') {
continue
}
const fieldType = simplifyType(field.type, ctx)
if (fieldType.kind !== 'union' && quickEqualTypes(fieldType, oldType)) {
const index = node.children.findIndex(pair => pair.key?.value === fieldKey.value.value.toString())
if (index !== -1) {
node.children.splice(index, 1)
}
const key: JsonStringNode = {
type: 'json:string',
range: node.range,
options: JsonStringOptions,
value: typeof field.key === 'string' ? field.key : fieldKey.value.value.toString(),
valueMap: [{ inner: core.Range.create(0), outer: core.Range.create(node.range.start) }],
}
const pair: JsonPairNode = {
type: 'pair',
range: node.range,
key,
value: oldNode,
children: [oldNode],
}
key.parent = pair
oldNode.parent = pair
node.children.push(pair)
pair.parent = node
return node
}
}
}
if (JsonObjectNode.is(oldNode) && oldType.kind === 'struct') {
// From {k: X} to X
for (const oldField of oldType.fields) {
const oldFieldKey = oldField.key
if (oldFieldKey.kind !== 'literal') {
continue
}
const oldFieldType = simplifyType(oldField.type, ctx)
if (oldFieldType.kind !== 'union' && quickEqualTypes(oldFieldType, type)) {
const oldPair = oldNode.children.find(pair => pair.key?.value === oldFieldKey.value.value.toString())
if (oldPair?.value) {
return oldPair.value
}
}
}
}
return node
}
export function isNumericType(type: McdocType): type is NumericType {
return type.kind === 'byte' || type.kind === 'short' || type.kind === 'int' || type.kind === 'long' || type.kind === 'float' || type.kind === 'double'
}
export function isListOrArray(type: McdocType): type is ListType | PrimitiveArrayType {
return type.kind === 'list' || type.kind === 'byte_array' || type.kind === 'int_array' || type.kind === 'long_array'
}
export function getItemType(type: ListType | PrimitiveArrayType): McdocType {
return type.kind === 'list' ? type.item
: type.kind === 'byte_array' ? { kind: 'byte' }
: type.kind === 'int_array' ? { kind: 'int' }
: type.kind === 'long_array' ? { kind: 'long' }
: { kind: 'any' }
}
export function isFixedList<T extends ListType | PrimitiveArrayType>(type: T): type is T & { lengthRange: NumericRange } {
return type.lengthRange?.min !== undefined && type.lengthRange.min === type.lengthRange.max
}
export function isInlineTuple(type: TupleType) {
return type.items.length <= 4 && type.items.every(isNumericType)
}
export function formatIdentifier(id: string, attributes?: Attributes): string {
if (id.startsWith('!')) {
return '! ' + formatIdentifier(id.substring(1), attributes)
}
const isStarred = attributes?.some(a => a.name === 'starred')
const text = id
.replace(/^minecraft:/, '')
.replaceAll('_', ' ')
.replace(/[a-z][A-Z]+/g, m => m.charAt(0) + ' ' + m.substring(1).toLowerCase())
return (isStarred ? '✨ ' : '') + text.charAt(0).toUpperCase() + text.substring(1)
}
export function getCategory(type: McdocType) {
if (type.kind === 'reference' && type.path) {
switch (type.path) {
case '::java::data::loot::LootPool':
case '::java::data::worldgen::dimension::Dimension':
case '::java::data::worldgen::surface_rule::SurfaceRule':
case '::java::data::worldgen::template_pool::WeightedElement':
return 'pool'
case '::java::data::loot::LootCondition':
case '::java::data::advancement::AdvancementCriterion':
case '::java::data::worldgen::dimension::biome_source::BiomeSource':
case '::java::data::worldgen::processor_list::ProcessorRule':
case '::java::data::worldgen::feature::placement::PlacementModifier':
return 'predicate'
case '::java::data::loot::LootFunction':
case '::java::data::worldgen::density_function::CubicSpline':
case '::java::data::worldgen::processor_list::Processor':
return 'function'
}
}
return undefined
}
const selectRegistries = new Set([
'block_predicate_type',
'chunk_status',
'consume_effect_type',
'creative_mode_tab',
'data_component_predicate_type',
'data_component_type',
'dialog_action_type',
'dialog_body_type',
'dialog_type',
'enchantment_effect_component_type',
'enchantment_entity_effect_type',
'enchantment_level_based_value_type',
'enchantment_location_based_effect_type',
'enchantment_provider_type',
'enchantment_value_effect_type',
'entity_sub_predicate_type',
'float_provider_type',
'frog_variant',
'height_provider_type',
'input_control_type',
'int_provider_type',
'item_sub_predicate_type',
'loot_condition_type',
'loot_function_type',
'loot_nbt_provider_type',
'loot_number_provider_type',
'loot_pool_entry_type',
'loot_score_provider_type',
'map_decoration_type',
'number_format_type',
'pos_rule_test',
'position_source_type',
'recipe_book_category',
'recipe_display',
'recipe_serializer',
'recipe_type',
'rule_block_entity_modifier',
'rule_test',
'slot_display',
'spawn_condition_type',
'stat_type',
'test_instance_type',
'test_environment_definition_type',
'trigger_type',
'worldgen/biome_source',
'worldgen/block_state_provider_type',
'worldgen/carver',
'worldgen/chunk_generator',
'worldgen/density_function_type',
'worldgen/feature',
'worldgen/feature_size_type',
'worldgen/foliage_placer_type',
'worldgen/material_condition',
'worldgen/material_rule',
'worldgen/placement_modifier_type',
'worldgen/pool_alias_binding',
'worldgen/root_placer_type',
'worldgen/structure_placement',
'worldgen/structure_pool_element',
'worldgen/structure_processor',
'worldgen/structure_type',
'worldgen/tree_decorator_type',
'worldgen/trunk_placer_type',
])
export function isSelectRegistry(registry: string) {
return selectRegistries.has(registry)
}
const defaultCollapsedTypes = new Set([
'::java::data::worldgen::surface_rule::SurfaceRule',
])
export function isDefaultCollapsedType(type: McdocType) {
if (type.kind === 'reference' && type.path) {
return defaultCollapsedTypes.has(type.path)
}
return false
}
interface SimplifyNodeContext {
key?: JsonStringNode
parent?: JsonObjectNode
}
export function simplifyType(type: McdocType, ctx: core.CheckerContext, { key, parent }: SimplifyNodeContext = {}): SimplifiedMcdocType {
const simplifyNode: SimplifyValueNode<JsonNode | undefined> = {
entryNode: {
parent: parent ? {
entryNode: {
parent: undefined,
runtimeKey: undefined,
},
node: {
originalNode: parent,
inferredType: inferType(parent),
},
} : undefined,
runtimeKey: key ? {
originalNode: key,
inferredType: inferType(key),
} : undefined,
},
node: {
originalNode: undefined,
inferredType: { kind: 'any' },
},
}
const context: McdocCheckerContext<JsonNode | undefined> = {
...ctx,
allowMissingKeys: false,
requireCanonical: false,
isEquivalent: () => false,
getChildren: (node) => {
if (JsonObjectNode.is(node)) {
return node.children.filter(kvp => kvp.key).map(kvp => ({
key: { originalNode: kvp.key!, inferredType: inferType(kvp.key!) },
possibleValues: kvp.value
? [{ originalNode: kvp.value, inferredType: inferType(kvp.value) }]
: [],
}))
}
return []
},
reportError: () => {},
attachTypeInfo: () => {},
nodeAttacher: () => {},
stringAttacher: () => {},
}
const result = simplify(type, { node: simplifyNode, ctx: context })
return result.typeDef
}
function inferType(node: JsonNode): Exclude<McdocType, UnionType> {
switch (node.type) {
case 'json:boolean':
return { kind: 'literal', value: { kind: 'boolean', value: node.value! } }
case 'json:number':
return {
kind: 'literal',
value: { kind: node.value.type, value: Number(node.value.value) },
}
case 'json:null':
return { kind: 'any' } // null is always invalid?
case 'json:string':
return { kind: 'literal', value: { kind: 'string', value: node.value } }
case 'json:array':
return { kind: 'list', item: { kind: 'any' } }
case 'json:object':
return { kind: 'struct', fields: [] }
}
}
export function quickEqualTypes(a: SimplifiedMcdocTypeNoUnion, b: SimplifiedMcdocTypeNoUnion): boolean {
if (a === b) {
return true
}
if (a.kind !== b.kind) {
return false
}
if (a.kind === 'literal' && b.kind === 'literal') {
return a.value.kind === b.value.kind && a.value.value === b.value.value
}
if (a.kind === 'struct' && b.kind === 'struct') {
// Compare the first key of both structs
const keyA = a.fields[0]?.key
const keyB = b.fields[0]?.key
return (!keyA && !keyB) || (keyA && keyB && quickEqualTypes(keyA, keyB))
}
// Types are of the same kind
return true
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,65 +1,93 @@
import type { DataModel } from '@mcschema/core'
import { Path } from '@mcschema/core'
import { useState } from 'preact/hooks'
import { useModel } from '../../hooks/index.js'
import type { VersionId } from '../../services/index.js'
import type { DocAndNode } from '@spyglassmc/core'
import { useErrorBoundary } from 'preact/hooks'
import { useDocAndNode } from '../../contexts/Spyglass.jsx'
import { useVersion } from '../../contexts/Version.jsx'
import { checkVersion } from '../../services/index.js'
import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, StructureSetPreview } from '../previews/index.js'
import { safeJsonParse } from '../../Utils.js'
import { ErrorPanel } from '../ErrorPanel.jsx'
import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, DialogPreview, ItemModelPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js'
export const HasPreview = ['loot_table', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'model']
export const HasPreview = ['loot_table', 'recipe', 'dialog', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'item_definition', 'model']
type PreviewPanelProps = {
model: DataModel | undefined,
version: VersionId,
id: string,
docAndNode: DocAndNode | undefined,
shown: boolean,
onError: (message: string) => unknown,
}
export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) {
const [, setCount] = useState(0)
export function PreviewPanel({ id, docAndNode: original, shown }: PreviewPanelProps) {
if (!original) return <></>
useModel(model, () => {
setCount(count => count + 1)
})
const docAndNode = useDocAndNode(original)
if (!model) return <></>
const data = model.get(new Path([]))
if (!data) return <></>
const [error, dismissError] = useErrorBoundary()
if (id === 'loot_table') {
return <LootTablePreview {...{ model, version, shown, data }} />
if (error) {
const previewError = new Error(`Preview error: ${error.message}`)
if (error.stack) {
previewError.stack = error.stack
}
return <ErrorPanel error={previewError} onDismiss={dismissError} />
}
if (id === 'dimension' && model.get(new Path(['generator', 'type']))?.endsWith('noise')) {
return <BiomeSourcePreview {...{ model, version, shown, data }} />
return <div class="h-full">
<PreviewContent key={id} id={id} docAndNode={docAndNode} shown={shown} />
</div>
}
type PreviewContentProps = {
docAndNode: DocAndNode,
id: string,
shown: boolean,
}
export function PreviewContent({ id, docAndNode, shown }: PreviewContentProps) {
const { version } = useVersion()
if (id === 'loot_table') {
return <LootTablePreview {...{ docAndNode, shown }} />
}
if (id === 'recipe') {
return <RecipePreview {...{ docAndNode, shown }} />
}
if (id === 'dialog') {
return <DialogPreview {...{ docAndNode, shown }} />
}
if (id === 'dimension' && safeJsonParse(docAndNode.doc.getText())?.generator?.type?.endsWith('noise')) {
return <BiomeSourcePreview {...{ docAndNode, shown }} />
}
if (id === 'worldgen/density_function') {
return <DensityFunctionPreview {...{ model, version, shown, data }} />
return <DensityFunctionPreview {...{ docAndNode, shown }} />
}
if (id === 'worldgen/noise') {
return <NoisePreview {...{ model, version, shown, data }} />
return <NoisePreview {...{ docAndNode, shown }} />
}
if (id === 'worldgen/noise_settings' && checkVersion(version, '1.18')) {
return <NoiseSettingsPreview {...{ model, version, shown, data }} />
return <NoiseSettingsPreview {...{ docAndNode, shown }} />
}
if ((id === 'worldgen/placed_feature' || (id === 'worldgen/configured_feature' && checkVersion(version, '1.16', '1.17')))) {
return <DecoratorPreview {...{ model, version, shown, data }} />
return <DecoratorPreview {...{ docAndNode, shown }} />
}
if (id === 'worldgen/structure_set' && checkVersion(version, '1.19')) {
return <StructureSetPreview {...{ model, version, shown, data }} />
return <StructureSetPreview {...{ docAndNode, shown }} />
}
if (id === 'block_definition') {
return <BlockStatePreview {...{ model, version, shown, data }} />
return <BlockStatePreview {...{ docAndNode, shown }} />
}
if (id === 'item_definition') {
return <ItemModelPreview {...{ docAndNode, shown }} />
}
if (id === 'model') {
return <ModelPreview {...{ model, version, shown, data }} />
return <ModelPreview {...{ docAndNode, shown }} />
}
return <></>

View File

@@ -1,19 +1,20 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { useCallback, useMemo, useState } from 'preact/hooks'
import config from '../../Config.js'
import type { Project } from '../../contexts/index.js'
import { disectFilePath, useLocale, useProject } from '../../contexts/index.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { useModal } from '../../contexts/Modal.jsx'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import type { VersionId } from '../../services/index.js'
import { DEFAULT_VERSION, parseSource } from '../../services/index.js'
import { message, readZip } from '../../Utils.js'
import { DEFAULT_VERSION } from '../../services/index.js'
import { PROJECTS_URI } from '../../services/Spyglass.js'
import { hexId, message, readZip } from '../../Utils.js'
import { Btn, BtnMenu, FileUpload, Octicon, TextInput } from '../index.js'
import { Modal } from '../Modal.js'
interface Props {
onClose: () => unknown,
}
export function ProjectCreation({ onClose }: Props) {
export function ProjectCreation() {
const { locale } = useLocale()
const { projects, createProject, changeProject, updateProject } = useProject()
const { hideModal } = useModal()
const { projects, createProject, changeProject } = useProject()
const { client } = useSpyglass()
const [name, setName] = useState('')
const [namespace, setNamespace] = useState('')
@@ -32,38 +33,28 @@ export function ProjectCreation({ onClose }: Props) {
}
}
const projectUpdater = useRef(updateProject)
useEffect(() => {
projectUpdater.current = updateProject
}, [updateProject])
const onCreate = () => {
const onCreate = useCallback(async () => {
setCreating(true)
createProject(name, namespace || undefined, version)
const rootUri = `${PROJECTS_URI}${hexId()}/`
await client.fs.mkdir(rootUri)
createProject({ name, namespace, version, storage: { type: 'indexeddb', rootUri } })
changeProject(name)
if (file) {
readZip(file).then(async (entries) => {
const project: Partial<Project> = { files: [] }
await Promise.all(entries.map(async (entry) => {
const file = disectFilePath(entry[0])
if (file) {
try {
const data = await parseSource(entry[1], 'json')
project.files!.push({ ...file, data })
} catch (e) {
console.error(`Failed parsing ${file.type} ${file.id}: ${message(e)}`)
}
}
await Promise.all(entries.map((entry) => {
const path = entry[0].startsWith('/') ? entry[0].slice(1) : entry[0]
return client.fs.writeFile(rootUri + path, entry[1])
}))
projectUpdater.current(project)
onClose()
}).catch(() => {
onClose()
hideModal()
}).catch((e) => {
// TODO: handle errors
console.warn(`Error importing data pack: ${message(e)}`)
hideModal()
})
} else {
onClose()
hideModal()
}
}
}, [createProject, changeProject, client, version, name, namespace, file])
const invalidName = useMemo(() => {
return projects.map(p => p.name.trim().toLowerCase()).includes(name.trim().toLowerCase())
@@ -75,7 +66,7 @@ export function ProjectCreation({ onClose }: Props) {
const versions = config.versions.map(v => v.id as VersionId).reverse()
return <Modal class="project-creation" onDismiss={onClose}>
return <Modal class="project-creation">
<p>{locale('project.create')}</p>
<div class="input-group">
<TextInput autofocus class={`btn btn-input${!creating && (invalidName || name.length === 0) ? ' invalid': ''}`} placeholder={locale('project.name')} value={name} onChange={setName} />
@@ -85,7 +76,7 @@ export function ProjectCreation({ onClose }: Props) {
<TextInput class={`btn btn-input${!creating && invalidNamespace ? ' invalid' : ''}`} placeholder={locale('project.namespace')} value={namespace} onChange={setNamespace} />
{!creating && invalidNamespace && <div class="status-icon danger tooltipped tip-e" aria-label={locale('project.namespace.invalid')}>{Octicon.issue_opened}</div>}
</div>
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')} data-cy="version-switcher">
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')}>
{versions.map(v =>
<Btn label={v} active={v === version} onClick={() => setVersion(v)} />
)}

View File

@@ -1,27 +1,30 @@
import { useCallback } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { useLocale, useProject } from '../../contexts/index.js'
import { useModal } from '../../contexts/Modal.jsx'
import { Btn } from '../Btn.js'
import { Modal } from '../Modal.js'
interface Props {
onClose: () => void,
}
export function ProjectDeletion({ onClose }: Props) {
export function ProjectDeletion() {
const { locale } = useLocale()
const { projects, project, deleteProject } = useProject()
const { hideModal } = useModal()
const { project, deleteProject } = useProject()
const doSave = () => {
Analytics.deleteProject(projects.length, project.files.length, 'menu')
deleteProject(project.name)
onClose()
}
const doSave = useCallback(() => {
if (!project) {
return
}
Analytics.deleteProject('menu')
deleteProject(project!.name)
hideModal()
}, [deleteProject, hideModal])
return <Modal class="file-modal" onDismiss={onClose}>
<p>{locale('project.delete_confirm.1', project.name)}</p>
return <Modal class="file-modal">
<p>{project && locale('project.delete_confirm.1', project.name)}</p>
<p><b>{locale('project.delete_confirm.2')}</b></p>
<div class="button-group">
<Btn icon="trashcan" label={locale('project.delete')} onClick={doSave} class="danger" />
<Btn label={locale('project.cancel')} onClick={onClose} />
<Btn label={locale('project.cancel')} onClick={hideModal} />
</div>
</Modal>
}

View File

@@ -1,115 +1,115 @@
import type { DataModel } from '@mcschema/core'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { Analytics } from '../../Analytics.js'
import { Identifier } from 'deepslate'
import { route } from 'preact-router'
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
import config from '../../Config.js'
import { Store } from '../../Store.js'
import { writeZip } from '../../Utils.js'
import { DRAFT_PROJECT, disectFilePath, getFilePath, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { DRAFT_PROJECT, getProjectRoot, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { useModal } from '../../contexts/Modal.jsx'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import { useFocus } from '../../hooks/useFocus.js'
import type { VersionId } from '../../services/index.js'
import { stringifySource } from '../../services/index.js'
import { cleanUrl, writeZip } from '../../Utils.js'
import { Btn } from '../Btn.js'
import { BtnMenu } from '../BtnMenu.js'
import { Octicon } from '../Octicon.jsx'
import type { TreeViewGroupRenderer, TreeViewLeafRenderer } from '../TreeView.js'
import { TreeView } from '../TreeView.js'
import { FileRenaming } from './FileRenaming.jsx'
import { ProjectCreation } from './ProjectCreation.jsx'
import { ProjectDeletion } from './ProjectDeletion.jsx'
interface Props {
model: DataModel | undefined,
version: VersionId,
id: string,
onError: (message: string) => unknown,
onRename: (file: { type: string, id: string }) => unknown,
onCreate: () => unknown,
onDeleteProject: () => unknown,
}
export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
const { locale } = useLocale()
export function ProjectPanel() {
const { version } = useVersion()
const { projects, project, changeProject, file, openFile, updateFile } = useProject()
const { locale } = useLocale()
const { showModal } = useModal()
const { projects, project, projectUri, setProjectUri, changeProject } = useProject()
const { client, service } = useSpyglass()
const [treeViewMode, setTreeViewMode] = useState(Store.getTreeViewMode())
const projectRoot = project ? getProjectRoot(project) : undefined
const changeTreeViewMode = useCallback((mode: string) => {
Store.setTreeViewMode(mode)
Analytics.setTreeViewMode(mode)
setTreeViewMode(mode)
}, [])
const disectEntry = useCallback((entry: string) => {
if (treeViewMode === 'resources' && entry !== 'pack.mcmeta') {
const [type, id] = entry.split('/')
return {
type: type.replaceAll('\u2215', '/'),
id: id.replaceAll('\u2215', '/'),
}
const [entries, setEntries] = useState<string[]>()
useEffect(() => {
setEntries(undefined)
if (!projectRoot) {
return
}
return disectFilePath(entry)
}, [treeViewMode])
const entries = useMemo(() => project.files.flatMap(f => {
const path = getFilePath(f)
if (!path) return []
if (f.type === 'pack_mcmeta') return 'pack.mcmeta'
if (treeViewMode === 'resources') {
return [`${f.type.replaceAll('/', '\u2215')}/${f.id.replaceAll('/', '\u2215')}`]
client.fs.readdir(projectRoot).then(entries => {
setEntries(entries.flatMap(e => {
return e.isFile() ? [e.name.slice(projectRoot.length)] : []
}))
})
}, [projectRoot])
useEffect(() => {
if (!service || !projectRoot) {
return
}
return [path]
}), [treeViewMode, ...project.files])
const selected = useMemo(() => file && getFilePath(file), [file])
const selectFile = useCallback((entry: string) => {
const file = disectEntry(entry)
if (file) {
openFile(file.type, file.id)
}
}, [disectEntry])
service.watchTree(projectRoot, setEntries)
return () => service.unwatchTree(projectRoot, setEntries)
}, [service, projectRoot])
const download = useRef<HTMLAnchorElement>(null)
const onDownload = async () => {
if (!download.current) return
let hasPack = false
const entries = project.files.flatMap(file => {
const path = getFilePath(file)
if (path === undefined) return []
if (path === 'pack.mcmeta') hasPack = true
return [[path, stringifySource(file.data)]] as [string, string][]
})
if (!hasPack) {
const pack_format = config.versions.find(v => v.id === version)!.pack_format
entries.push(['pack.mcmeta', stringifySource({ pack: { pack_format, description: '' } })])
if (!download.current || entries === undefined || !project) {
return
}
const url = await writeZip(entries)
const zipEntries = await Promise.all(entries.map(async e => {
const data = await client.fs.readFile(projectRoot + e)
return [e, data] as [string, Uint8Array]
}))
if (!zipEntries.some(e => e[0] === 'pack.mcmeta')) {
const packFormat = config.versions.find(v => v.id === version)!.pack_format
const packMcmeta = { pack: { description: project.name, pack_format: packFormat } }
const data = new TextEncoder().encode(JSON.stringify(packMcmeta, null, 2))
zipEntries.push(['pack.mcmeta', data])
}
const url = await writeZip(zipEntries)
download.current.setAttribute('href', url)
download.current.setAttribute('download', `${project.name.replaceAll(' ', '_')}.zip`)
download.current.click()
}
const onDeleteProject = useCallback(() => {
showModal(() => <ProjectDeletion />)
}, [])
const onCreateProject = useCallback(() => {
showModal(() => <ProjectCreation />)
}, [])
const actions = useMemo(() => [
{
icon: 'pencil',
label: locale('project.rename_file'),
onAction: (entry: string) => {
const file = disectEntry(entry)
if (file) {
onRename(file)
onAction: (uri: string) => {
const res = service?.dissectUri(uri)
if (res) {
// This is pretty hacky, improve this in the future when spyglass has a "constructUri" function
const oldSuffix = `${res.pack}/${res.namespace}/${res.path}/${res.identifier}${res.ext}`
if (!uri.endsWith(oldSuffix)) {
console.warn(`Expected ${uri} to end with ${oldSuffix}`)
return
}
const onRename = (newId: string) => {
const prefix = uri.substring(0, uri.length - oldSuffix.length)
const { namespace, path } = Identifier.parse(newId)
const newUri = prefix + `${res.pack}/${namespace}/${res.path}/${path}${res.ext}`
service?.renameFile(uri, newUri).then(() => {
setProjectUri(newUri)
})
}
showModal(() => <FileRenaming oldId={`${res.namespace}:${res.identifier}`} onRename={onRename} />)
}
},
},
{
icon: 'trashcan',
label: locale('project.delete_file'),
onAction: (entry: string) => {
const file = disectEntry(entry)
if (file) {
Analytics.deleteProjectFile(file.type, projects.length, project.files.length, 'menu')
updateFile(file.type, file.id, {})
}
onAction: (uri: string) => {
client.fs.unlink(uri).then(() => {
setProjectUri(undefined)
})
},
},
], [disectEntry, updateFile, onRename])
], [client, service, projectRoot, showModal])
const FolderEntry: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => {
return <div class="entry" onClick={onClick} >
@@ -120,41 +120,59 @@ export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
const FileEntry: TreeViewLeafRenderer<string> = useCallback(({ entry }) => {
const [focused, setFocus] = useFocus()
const uri = projectRoot + entry
const onContextMenu = (evt: MouseEvent) => {
evt.preventDefault()
setFocus()
}
const file = disectEntry(entry)
const onClick = () => {
const category = uri.endsWith('/pack.mcmeta')
? 'pack_mcmeta'
: service?.dissectUri(uri)?.category
const gen = config.generators.find(g => g.id === category)
if (!gen) {
throw new Error(`Cannot find generator for uri ${uri}`)
}
route(cleanUrl(gen.url))
setProjectUri(uri)
}
return <div class={`entry ${file && getFilePath(file) === selected ? 'active' : ''} ${focused ? 'focused' : ''}`} onClick={() => selectFile(entry)} onContextMenu={onContextMenu} >
return <div class={`entry ${uri === projectUri ? 'active' : ''} ${focused ? 'focused' : ''}`} onClick={onClick} onContextMenu={onContextMenu} >
{Octicon.file}
<span>{entry.split('/').at(-1)}</span>
{focused && <div class="entry-menu">
{actions?.map(a => <div class="action [&>svg]:inline" onClick={e => { a.onAction(entry); e.stopPropagation(); setFocus(false) }}>
{actions?.map(a => <div class="action [&>svg]:inline" onClick={e => { a.onAction(uri); e.stopPropagation(); setFocus(false) }}>
{(Octicon as any)[a.icon]}
<span>{a.label}</span>
</div>)}
</div>}
</div>
}, [actions, disectEntry])
}, [service, actions, projectRoot, projectUri])
return <>
return <div class="panel-content">
<div class="project-controls">
<BtnMenu icon="chevron_down" label={project.name} tooltip={locale('switch_project')} tooltipLoc="se">
{projects.map(p => <Btn label={p.name} active={p.name === project.name} onClick={() => changeProject(p.name)} />)}
<BtnMenu icon="chevron_down" label={project ? project.name : locale('loading')} tooltip={locale('switch_project')} tooltipLoc="se">
{projects.map(p => <Btn label={p.name} active={p.name === project?.name} onClick={() => changeProject(p.name)} />)}
</BtnMenu>
<BtnMenu icon="kebab_horizontal" >
<Btn icon="file_zip" label={locale('project.download')} onClick={onDownload} />
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreate} />
<Btn icon={treeViewMode === 'resources' ? 'three_bars' : 'rows'} label={locale(treeViewMode === 'resources' ? 'project.show_file_paths' : 'project.show_resources')} onClick={() => changeTreeViewMode(treeViewMode === 'resources' ? 'files' : 'resources')} />
{project.name !== DRAFT_PROJECT.name && <Btn icon="trashcan" label={locale('project.delete')} onClick={onDeleteProject} />}
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreateProject} />
{(project && project.name !== DRAFT_PROJECT.name) && <Btn icon="trashcan" label={locale('project.delete')} onClick={onDeleteProject} />}
</BtnMenu>
</div>
<div class="file-view">
{entries.length === 0
? <span>{locale('project.no_files')}</span>
: <TreeView entries={entries} split={path => path.split('/')} group={FolderEntry} leaf={FileEntry} />}
<div class="project-files">
{entries === undefined
? <div class="p-2 flex flex-col gap-2">
<div class="skeleton-2 rounded h-4 w-24"></div>
<div class="skeleton-2 rounded h-4 w-32 ml-4"></div>
<div class="skeleton-2 rounded h-4 w-24 ml-8"></div>
<div class="skeleton-2 rounded h-4 w-36 ml-8"></div>
<div class="skeleton-2 rounded h-4 w-28"></div>
</div>
: entries.length === 0
? <span>{locale('project.no_files')}</span>
: <TreeView entries={entries} split={path => path.split('/')} group={FolderEntry} leaf={FileEntry} />}
</div>
<a ref={download} style="display: none;"></a>
</>
</div>
}

View File

@@ -1,19 +1,24 @@
import { DataModel, Path } from '@mcschema/core'
import { route } from 'preact-router'
import { useCallback, useEffect, useErrorBoundary, useMemo, useRef, useState } from 'preact/hooks'
import type { Method } from '../../Analytics.js'
import { Analytics } from '../../Analytics.js'
import type { ConfigGenerator } from '../../Config.js'
import config from '../../Config.js'
import { Store } from '../../Store.js'
import { cleanUrl, deepEqual } from '../../Utils.js'
import { DRAFT_PROJECT, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { AsyncCancel, useActiveTimeout, useAsync, useModel, useSearchParam } from '../../hooks/index.js'
import { getOutput } from '../../schema/transformOutput.js'
import { useModal } from '../../contexts/Modal.jsx'
import { useSpyglass, watchSpyglassUri } from '../../contexts/Spyglass.jsx'
import { AsyncCancel, useActiveTimeout, useAsync, useLocalStorage, useSearchParam } from '../../hooks/index.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchPreset, getBlockStates, getCollections, getModel, getSnippet, shareSnippet } from '../../services/index.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js'
import { checkVersion, fetchDependencyMcdoc, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js'
import { DEPENDENCY_URI } from '../../services/Spyglass.js'
import { Store } from '../../Store.js'
import { cleanUrl, genPath } from '../../Utils.js'
import { FancyMenu } from '../FancyMenu.jsx'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileView, Footer, HasPreview, Octicon, PreviewPanel, ProjectPanel, SourcePanel, TextInput, VersionSwitcher } from '../index.js'
import { getRootDefault } from './McdocHelpers.js'
export const SHARE_KEY = 'share'
const MIN_PROJECT_PANEL_WIDTH = 200
interface Props {
gen: ConfigGenerator
@@ -22,35 +27,50 @@ interface Props {
export function SchemaGenerator({ gen, allowedVersions }: Props) {
const { locale } = useLocale()
const { version, changeVersion, changeTargetVersion } = useVersion()
const { projects, project, file, updateProject, updateFile, closeFile } = useProject()
const { service } = useSpyglass()
const { showModal } = useModal()
const { project, projectUri, setProjectUri, updateProject } = useProject()
const [error, setError] = useState<Error | string | null>(null)
const [errorBoundary, errorRetry] = useErrorBoundary()
if (errorBoundary) {
errorBoundary.message = `Something went wrong rendering the generator: ${errorBoundary.message}`
return <main><ErrorPanel error={errorBoundary} onDismiss={errorRetry} /></main>
const generatorError = new Error(`Generator error: ${errorBoundary.message}`)
if (errorBoundary.stack) {
generatorError.stack = errorBoundary.stack
}
return <main><ErrorPanel error={generatorError} onDismiss={errorRetry} /></main>
}
useEffect(() => Store.visitGenerator(gen.id), [gen.id])
const uri = useMemo(() => {
if (!service) {
return undefined
}
if (projectUri) {
const category = projectUri.endsWith('/pack.mcmeta')
? 'pack_mcmeta'
: service.dissectUri(projectUri)?.category
if (category === gen.id) {
return projectUri
} else {
setProjectUri(undefined)
}
}
return service.getUnsavedFileUri(gen)
}, [service, version, gen, projectUri])
const [currentPreset, setCurrentPreset] = useSearchParam('preset')
const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY)
const ignoreChange = useRef(false)
const backup = useMemo(() => Store.getBackup(gen.id), [gen.id])
const loadBackup = () => {
if (backup !== undefined) {
model?.reset(DataModel.wrapLists(backup), false)
}
}
const { value } = useAsync(async () => {
let data: unknown = undefined
const { value: docAndNode, loading: docLoading, error: docError } = useAsync(async () => {
let text: string | undefined = undefined
if (currentPreset && sharedSnippetId) {
setSharedSnippetId(undefined)
return AsyncCancel
}
if (currentPreset) {
data = await loadPreset(currentPreset)
text = await loadPreset(currentPreset)
} else if (sharedSnippetId) {
const snippet = await getSnippet(sharedSnippetId)
let cancel = false
@@ -73,91 +93,130 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
setSourceShown(false)
}
Analytics.openSnippet(gen.id, sharedSnippetId, version)
data = snippet.data
} else if (file) {
if (project.version && project.version !== version) {
changeVersion(project.version, false)
return AsyncCancel
}
data = file.data
text = snippet.text
}
const [model, blockStates] = await Promise.all([
getModel(version, gen.id),
getBlockStates(version),
])
if (data) {
if (!service || !uri) {
return AsyncCancel
}
// TODO: clear the dependencies that are not used
// Right now if you do this, the mcdoc breaks when switching back to the dependency later
if (gen.dependency) {
const dependency = await fetchDependencyMcdoc(gen.dependency)
const dependencyUri = `${DEPENDENCY_URI}${gen.dependency}.mcdoc`
await service.writeFile(dependencyUri, dependency)
}
if (text !== undefined) {
ignoreChange.current = true
model.reset(DataModel.wrapLists(data), false)
await service.writeFile(uri, text)
ignoreChange.current = false
} else {
text = await service.readFile(uri)
if (text === undefined) {
const node = getRootDefault(gen.id, service.getCheckerContext())
text = service.formatNode(node, uri)
await service.writeFile(uri, text)
}
}
ignoreChange.current = true
const docAndNode = await service.openFile(uri)
ignoreChange.current = false
Analytics.setGenerator(gen.id)
return { model, blockStates }
}, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id])
return docAndNode
}, [gen.id, version, sharedSnippetId, currentPreset, service, uri])
const model = value?.model
const blockStates = value?.blockStates
const { doc } = docAndNode ?? {}
useModel(model, model => {
watchSpyglassUri(uri, () => {
if (!ignoreChange.current) {
setCurrentPreset(undefined, true)
setSharedSnippetId(undefined, true)
}
if (file && model && blockStates) {
const data = getOutput(model, blockStates)
updateFile(gen.id, file.id, { id: file.id, data })
}
ignoreChange.current = false
Store.setBackup(gen.id, DataModel.unwrapLists(model.data))
setError(null)
}, [gen.id, setCurrentPreset, setSharedSnippetId, blockStates, file?.id])
}, [])
const reset = () => {
Analytics.resetGenerator(gen.id, model?.historyIndex ?? 1, 'menu')
model?.reset(DataModel.wrapLists(model.schema.default()), true)
const reset = async () => {
if (!service || !uri) {
return
}
Analytics.resetGenerator(gen.id, 1, 'menu')
const node = getRootDefault(gen.id, service.getCheckerContext())
const newText = service.formatNode(node, uri)
await service.writeFile(uri, newText)
}
const undo = (e: MouseEvent) => {
const undo = async (e: MouseEvent) => {
e.stopPropagation()
Analytics.undoGenerator(gen.id, model?.historyIndex ?? 1, 'menu')
model?.undo()
if (!service || !uri) {
return
}
Analytics.undoGenerator(gen.id, 1, 'menu')
await service.undoEdit(uri)
}
const redo = (e: MouseEvent) => {
const redo = async (e: MouseEvent) => {
e.stopPropagation()
Analytics.redoGenerator(gen.id, model?.historyIndex ?? 1, 'menu')
model?.redo()
if (!service || !uri) {
return
}
Analytics.redoGenerator(gen.id, 1, 'menu')
await service?.redoEdit(uri)
}
const onKeyUp = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'z') {
Analytics.undoGenerator(gen.id, model?.historyIndex ?? 1, 'hotkey')
model?.undo()
} else if (e.ctrlKey && e.key === 'y') {
Analytics.redoGenerator(gen.id, model?.historyIndex ?? 1, 'hotkey')
model?.redo()
const saveFile = useCallback((method: Method) => {
if (!docAndNode) {
return
}
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 's') {
setFileSaving('hotkey')
e.preventDefault()
e.stopPropagation()
}
}
showModal(() => <FileCreation gen={gen} docAndNode={docAndNode} method={method} />)
}, [showModal, gen, docAndNode])
useEffect(() => {
document.addEventListener('keyup', onKeyUp)
const onKeyDown = async (e: KeyboardEvent) => {
if (!service || !uri) {
return
}
if (e.ctrlKey && e.key === 'z') {
e.preventDefault()
Analytics.undoGenerator(gen.id, 1, 'hotkey')
await service.undoEdit(uri)
} else if (e.ctrlKey && e.key === 'y') {
e.preventDefault()
Analytics.redoGenerator(gen.id, 1, 'hotkey')
await service.redoEdit(uri)
} else if (e.ctrlKey && e.key === 's') {
saveFile('hotkey')
e.preventDefault()
e.stopPropagation()
}
}
document.addEventListener('keydown', onKeyDown)
return () => {
document.removeEventListener('keyup', onKeyUp)
document.removeEventListener('keydown', onKeyDown)
}
}, [model, blockStates, file])
}, [gen.id, service, uri, saveFile])
const [presets, setPresets] = useState<string[]>([])
useEffect(() => {
getCollections(version).then(collections => {
setPresets(collections.get(gen.id).map(p => p.startsWith('minecraft:') ? p.slice(10) : p))
})
.catch(e => { console.error(e); setError(e) })
const { value: presets } = useAsync(async () => {
const registries = await fetchRegistries(version)
const entries = registries.get(gen.id) ?? []
return entries.map(e => e.startsWith('minecraft:') ? e.slice(10) : e)
}, [version, gen.id])
const getPresets = useCallback((search: string, close: () => void) => {
if (presets === undefined) {
return <span class="w-80 note">{locale('loading')}</span>
}
if (!presets || presets.length === 0) {
return <span class="w-80 note">{locale('presets.no_results')}</span>
}
const terms = search.trim().split(' ')
const results = presets?.filter(v => terms.every(t => v.includes(t))).slice(0, 100) ?? []
if (results.length === 0) {
return <span class="w-80 note">{locale('presets.no_results_for_query')}</span>
}
return results.map(r => <button class="w-80 flex items-center cursor-pointer no-underline rounded p-1" onClick={() => {selectPreset(r); close()}}>
{r}
</button>)
}, [presets])
const selectPreset = (id: string) => {
Analytics.loadPreset(gen.id, id)
setSharedSnippetId(undefined, true)
@@ -167,25 +226,18 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
const loadPreset = async (id: string) => {
try {
const preset = await fetchPreset(version, gen.path ?? gen.id, id)
const seed = model?.get(new Path(['generator', 'seed']))
if (preset?.generator?.seed !== undefined && seed !== undefined) {
preset.generator.seed = seed
if (preset.generator.biome_source?.seed !== undefined) {
preset.generator.biome_source.seed = seed
}
}
return preset
return await fetchPreset(version, genPath(gen, version), id)
} catch (e) {
setError(`Cannot load preset ${id} in ${version}`)
setCurrentPreset(undefined, true)
return undefined
}
}
const selectVersion = (version: VersionId) => {
setSharedSnippetId(undefined, true)
changeVersion(version)
if (project.name !== DRAFT_PROJECT.name && project.version !== version) {
if (project && project.name !== DRAFT_PROJECT.name && project.version !== version) {
updateProject({ version })
}
}
@@ -203,27 +255,21 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
setShareUrl(`${location.origin}/${gen.url}/?version=${version}&preset=${currentPreset}`)
setShareShown(true)
copySharedId()
} else if (model && blockStates) {
const output = getOutput(model, blockStates)
if (deepEqual(output, model.schema.default())) {
setShareUrl(`${location.origin}/${gen.url}/?version=${version}`)
setShareShown(true)
} else {
setShareLoading(true)
shareSnippet(gen.id, version, output, previewShown)
.then(({ id, length, compressed, rate }) => {
Analytics.createSnippet(gen.id, id, version, length, compressed, rate)
const url = `${location.origin}/${gen.url}/?${SHARE_KEY}=${id}`
setShareUrl(url)
setShareShown(true)
})
.catch(e => {
if (e instanceof Error) {
setError(e)
}
})
.finally(() => setShareLoading(false))
}
} else if (doc) {
setShareLoading(true)
shareSnippet(gen.id, version, doc.getText(), previewShown)
.then(({ id, length, compressed, rate }) => {
Analytics.createSnippet(gen.id, id, version, length, compressed, rate)
const url = `${location.origin}/${gen.url}/?${SHARE_KEY}=${id}`
setShareUrl(url)
setShareShown(true)
})
.catch(e => {
if (e instanceof Error) {
setError(e)
}
})
.finally(() => setShareLoading(false))
}
}
const copySharedId = () => {
@@ -277,66 +323,88 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
} else {
Analytics.showPreview(gen.id, 'menu')
}
Store.setPreviewPanelOpen(!previewShown)
setPreviewShown(!previewShown)
if (!previewShown && sourceShown) {
setSourceShown(false)
}
}
const [projectShown, setProjectShown] = useState(Store.getProjectPanelOpen() ?? window.innerWidth > 1000)
const [projectShown, setProjectShown] = useState(Store.getProjectPanelOpen() ?? false)
const toggleProjectShown = useCallback(() => {
if (projectShown) {
Analytics.hideProject(gen.id, projects.length, project.files.length, 'menu')
Analytics.hideProject('menu')
} else {
Analytics.showProject(gen.id, projects.length, project.files.length, 'menu')
Analytics.showProject('menu')
}
Store.setProjectPanelOpen(!projectShown)
setProjectShown(!projectShown)
}, [projectShown])
const [projectCreating, setProjectCreating] = useState(false)
const [projectDeleting, setprojectDeleting] = useState(false)
const [fileSaving, setFileSaving] = useState<string | undefined>(undefined)
const [fileRenaming, setFileRenaming] = useState<{ type: string, id: string } | undefined>(undefined)
const [newFileQueued, setNewFileQueued] = useState(false)
const onNewFile = useCallback(() => {
closeFile()
// Need to queue reset because otherwise the useModel hook will update the old file
setNewFileQueued(true)
}, [closeFile])
const [panelWidth, setPanelWidth] = useLocalStorage('misode_project_panel_width', MIN_PROJECT_PANEL_WIDTH, (s) => Number(s), (v) => v.toString())
const [realPanelWidth, setRealPanelWidth] = useState(panelWidth)
const [resizeStart, setResizeStart] = useState<number>()
useEffect(() => {
if (file === undefined && newFileQueued) {
model?.reset(DataModel.wrapLists(model.schema.default()), true)
setNewFileQueued(false)
const onMouseMove = (e: MouseEvent) => {
if (resizeStart) {
const targetWidth = e.clientX - resizeStart
if (targetWidth < 50) {
setProjectShown(false)
} else {
setRealPanelWidth(Math.max(MIN_PROJECT_PANEL_WIDTH, targetWidth))
}
}
}
}, [model, newFileQueued, file])
window.addEventListener('mousemove', onMouseMove)
return () => window.removeEventListener('mousemove', onMouseMove)
}, [resizeStart])
useEffect(() => {
const onMouseUp = () => {
setResizeStart(undefined)
if (realPanelWidth < MIN_PROJECT_PANEL_WIDTH) {
setRealPanelWidth(panelWidth)
} else {
setPanelWidth(realPanelWidth)
}
}
window.addEventListener('mouseup', onMouseUp)
return () => window.removeEventListener('mouseup', onMouseUp)
}, [panelWidth, realPanelWidth])
const newEmptyFile = useCallback(async () => {
if (service) {
const unsavedUri = service.getUnsavedFileUri(gen)
const node = getRootDefault(gen.id, service.getCheckerContext())
const text = service.formatNode(node, unsavedUri)
await service.writeFile(unsavedUri, text)
}
setProjectUri(undefined)
}, [gen, service, showModal])
return <>
<main class={`generator${previewShown ? ' has-preview' : ''}${projectShown ? ' has-project' : ''}`}>
{!gen.partner && <Ad id="data-pack-generator" type="text" />}
<main class={`${previewShown ? 'has-preview' : ''} ${projectShown ? 'has-project' : ''}`} style={`--project-panel-width: ${realPanelWidth}px`}>
{!gen.tags?.includes('partners') && <Ad id="data-pack-generator" type="text" />}
<div class="controls generator-controls">
{gen.wiki && <a class="btn btn-link tooltipped tip-se" aria-label={locale('learn_on_the_wiki')} href={`https://minecraft.wiki/w/${gen.wiki}`} target="_blank">
{gen.wiki && <a class="btn btn-link tooltipped tip-se" aria-label={locale('learn_on_the_wiki')} href={gen.wiki} target="_blank">
{Octicon.mortar_board}
<span>{locale('wiki')}</span>
</a>}
<BtnMenu icon="archive" label={locale('presets')} relative={false}>
<SearchList searchPlaceholder={locale('search')} noResults={locale('no_presets')} values={presets} onSelect={selectPreset}/>
</BtnMenu>
<FancyMenu placeholder={locale('search')} getResults={getPresets} relative={false} class="right-0 mt-2">
<Btn icon="archive" label={locale('presets')} />
</FancyMenu>
<VersionSwitcher value={version} onChange={selectVersion} allowed={allowedVersions} />
<BtnMenu icon="kebab_horizontal" tooltip={locale('more')}>
<Btn icon="history" label={locale('reset_default')} onClick={reset} />
{backup !== undefined && <Btn icon="history" label={locale('restore_backup')} onClick={loadBackup} />}
<Btn icon="arrow_left" label={locale('undo')} onClick={undo} />
<Btn icon="arrow_right" label={locale('redo')} onClick={redo} />
<Btn icon="plus_circle" label={locale('project.new_file')} onClick={onNewFile} />
<Btn icon="file" label={locale('project.save')} onClick={() => setFileSaving('menu')} />
<Btn icon="plus_circle" label={locale('project.new_file')} onClick={newEmptyFile} />
<Btn icon="file" label={locale('project.save')} onClick={() => saveFile('menu')} />
</BtnMenu>
</div>
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
<Tree {...{model, version, blockStates}} onError={setError} />
<Footer donate={!gen.partner} />
{docError
? <ErrorPanel error={docError} />
: <FileView docAndNode={docLoading ? undefined : docAndNode} />}
<Footer donate={!gen.tags?.includes('partners')} />
</main>
<div class="popup-actions right-actions" style={`--offset: -${8 + actionsShown * 50}px;`}>
<div class={`popup-action action-preview${hasPreview ? ' shown' : ''} tooltipped tip-nw`} aria-label={locale(previewShown ? 'hide_preview' : 'show_preview')} onClick={togglePreview}>
@@ -349,33 +417,30 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
{Octicon.download}
</div>
<div class={`popup-action action-copy${sourceShown ? ' shown' : ''}${copyActive ? ' active' : ''} tooltipped tip-nw`} aria-label={locale(copyActive ? 'copied' : 'copy')} onClick={copySource}>
{copyActive ? Octicon.check : Octicon.clippy}
{copyActive ? Octicon.check : Octicon.copy}
</div>
<div class={'popup-action action-code shown tooltipped tip-nw'} aria-label={locale(sourceShown ? 'hide_output' : 'show_output')} onClick={toggleSource}>
{sourceShown ? Octicon.chevron_right : Octicon.code}
</div>
</div>
<div class={`popup-preview${previewShown ? ' shown' : ''}`}>
<PreviewPanel {...{model, version, id: gen.id}} shown={previewShown} onError={setError} />
<PreviewPanel docAndNode={docAndNode} id={gen.id} shown={previewShown} />
</div>
<div class={`popup-source${sourceShown ? ' shown' : ''}`}>
<SourcePanel {...{model, blockStates, doCopy, doDownload, doImport}} name={gen.schema ?? 'data'} copySuccess={copySuccess} onError={setError} />
<SourcePanel docAndNode={docAndNode} {...{doCopy, doDownload, doImport}} copySuccess={copySuccess} onError={setError} />
</div>
<div class={`popup-share${shareShown ? ' shown' : ''}`}>
<TextInput value={shareUrl} readonly />
<Btn icon={shareCopyActive ? 'check' : 'clippy'} onClick={copySharedId} tooltip={locale(shareCopyActive ? 'copied' : 'copy_share')} tooltipLoc="nw" active={shareCopyActive} />
<Btn icon={shareCopyActive ? 'check' : 'copy'} onClick={copySharedId} tooltip={locale(shareCopyActive ? 'copied' : 'copy_share')} tooltipLoc="nw" active={shareCopyActive} />
</div>
<div class="popup-actions left-actions" style="--offset: 50px;">
<div class={'popup-action action-project shown tooltipped tip-ne'} aria-label={locale(projectShown ? 'hide_project' : 'show_project')} onClick={toggleProjectShown}>
{projectShown ? Octicon.chevron_left : Octicon.repo}
</div>
</div>
<div class={`popup-project${projectShown ? ' shown' : ''}`}>
<ProjectPanel {...{model, version, id: gen.id}} onError={setError} onDeleteProject={() => setprojectDeleting(true)} onRename={setFileRenaming} onCreate={() => setProjectCreating(true)} />
<div class={`popup-project${projectShown ? ' shown' : ''}`} style={`width: ${realPanelWidth}px`}>
<ProjectPanel/>
<div class="panel-resize" onMouseDown={(e) => setResizeStart(e.clientX - panelWidth)}></div>
</div>
{projectCreating && <ProjectCreation onClose={() => setProjectCreating(false)} />}
{projectDeleting && <ProjectDeletion onClose={() => setprojectDeleting(false)} />}
{model && fileSaving && <FileCreation id={gen.id} model={model} method={fileSaving} onClose={() => setFileSaving(undefined)} />}
{fileRenaming && <FileRenaming id={fileRenaming.type } name={fileRenaming.id} onClose={() => setFileRenaming(undefined)} />}
</>
}

View File

@@ -1,9 +1,9 @@
import { DataModel } from '@mcschema/core'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import type { DocAndNode } from '@spyglassmc/core'
import { fileUtil } from '@spyglassmc/core'
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { useModel } from '../../hooks/index.js'
import { getOutput } from '../../schema/transformOutput.js'
import type { BlockStateRegistry } from '../../services/index.js'
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
import { useLocalStorage } from '../../hooks/index.js'
import { getSourceFormats, getSourceIndent, getSourceIndents, parseSource, stringifySource } from '../../services/index.js'
import { Store } from '../../Store.js'
import { message } from '../../Utils.js'
@@ -12,44 +12,54 @@ import { Btn, BtnMenu } from '../index.js'
interface Editor {
getValue(): string
setValue(value: string): void
configure(indent: string, format: string): void
configure(indent: string, format: string, wrap: boolean): void
select(): void
}
type SourcePanelProps = {
name: string,
model: DataModel | undefined,
blockStates: BlockStateRegistry | undefined,
docAndNode: DocAndNode | undefined,
doCopy?: number,
doDownload?: number,
doImport?: number,
copySuccess: () => unknown,
onError: (message: string | Error) => unknown,
}
export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
export function SourcePanel({ docAndNode, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
const { locale } = useLocale()
const [indent, setIndent] = useState(Store.getIndent())
const [format, setFormat] = useState(Store.getFormat())
const { service } = useSpyglass()
const [cIndent, setIndent] = useState(Store.getIndent())
const [cFormat, setFormat] = useState(Store.getFormat())
const [inline, setInline] = useLocalStorage('misode_output_inline', false, (s) => s === 'true', (b) => b ? 'true' : 'false')
// const [sort, setSort] = useLocalStorage('misode_output_sort', 'schema')
const [highlighting, setHighlighting] = useState(Store.getHighlighting())
const [braceLoaded, setBraceLoaded] = useState(false)
const download = useRef<HTMLAnchorElement>(null)
const retransform = useRef<Function>(() => {})
const onImport = useRef<() => Promise<void>>(async () => {})
const outputRef = useRef<string>()
const textarea = useRef<HTMLTextAreaElement>(null)
const editor = useRef<Editor>()
const getSerializedOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
const data = getOutput(model, blockStates)
return stringifySource(data, format, indent)
const { indent, format } = useMemo(() => {
return inline ? { indent: 'minified', format: 'snbt' } : { indent: cIndent, format: cFormat }
}, [cIndent, cFormat, inline])
const getSerializedOutput = useCallback((text: string) => {
// TODO: implement sort
return stringifySource(text, format, indent)
}, [indent, format])
const text = useDocAndNode(docAndNode)?.doc.getText()
useEffect(() => {
retransform.current = () => {
if (!editor.current) return
if (!model || !blockStates) return
if (!editor.current || text === undefined) {
return
}
try {
const output = getSerializedOutput(model, blockStates)
const output = getSerializedOutput(text)
outputRef.current = output
editor.current.setValue(output)
} catch (e) {
if (e instanceof Error) {
@@ -67,9 +77,10 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
if (!editor.current) return
const value = editor.current.getValue()
if (value.length === 0) return
if (!service || !docAndNode) return
try {
const data = await parseSource(value, format)
model?.reset(DataModel.wrapLists(data), false)
const text = await parseSource(value, format)
await service.writeFile(docAndNode.doc.uri, text)
} catch (e) {
if (e instanceof Error) {
e.message = `Error importing: ${e.message}`
@@ -80,7 +91,7 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
console.error(e)
}
}
}, [model, blockStates, indent, format, highlighting])
}, [service, docAndNode, text, indent, format, highlighting])
useEffect(() => {
if (highlighting) {
@@ -101,6 +112,8 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
fontSize: 14,
showFoldWidgets: false,
highlightSelectedWord: false,
scrollPastEnd: 0.5,
indentedSoftWrap: false,
})
braceEditor.$blockScrolling = Infinity
braceEditor.on('blur', () => onImport.current())
@@ -113,7 +126,8 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
setValue(value) {
braceEditor.getSession().setValue(value)
},
configure(indent, format) {
configure(indent, format, wrap) {
braceEditor.setOption('wrap', wrap)
braceEditor.setOption('useSoftTabs', indent !== 'tabs')
braceEditor.setOption('tabSize', indent === 'tabs' ? 4 : getSourceIndent(indent))
braceEditor.getSession().setMode(`ace/mode/${format}`)
@@ -134,46 +148,47 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
if (!textarea.current) return
textarea.current.value = value
},
configure() {},
configure(_indent, _format, wrap) {
if (!textarea.current) return
textarea.current.style.wordBreak = wrap ? 'break-all' : 'unset'
textarea.current.style.whiteSpace = wrap ? 'wrap' : 'pre'
},
select() {},
}
}
}, [highlighting])
useModel(model, () => {
if (!retransform.current) return
retransform.current()
})
useEffect(() => {
if (!retransform.current) return
if (model) retransform.current()
}, [model])
if (retransform.current && text !== undefined) {
retransform.current()
}
}, [text])
useEffect(() => {
if (!editor.current || !retransform.current) return
if (!highlighting || braceLoaded) {
editor.current.configure(indent, format)
editor.current.configure(indent, format === 'snbt' ? 'yaml' : format, inline)
retransform.current()
}
}, [indent, format, highlighting, braceLoaded])
}, [indent, format, inline, highlighting, braceLoaded])
useEffect(() => {
if (doCopy && model && blockStates) {
navigator.clipboard.writeText(getSerializedOutput(model, blockStates)).then(() => {
if (doCopy && outputRef.current) {
navigator.clipboard.writeText(outputRef.current).then(() => {
copySuccess()
})
}
}, [doCopy])
}, [doCopy, textarea])
useEffect(() => {
if (doDownload && model && blockStates && download.current) {
const content = encodeURIComponent(getSerializedOutput(model, blockStates))
if (doDownload && docAndNode && outputRef.current && download.current) {
const content = encodeURIComponent(outputRef.current)
download.current.setAttribute('href', `data:text/json;charset=utf-8,${content}`)
const fileName = name === 'pack_mcmeta' ? 'pack.mcmeta' : `${name}.${format}`
const fileName = fileUtil.basename(docAndNode.doc.uri)
download.current.setAttribute('download', fileName)
download.current.click()
}
}, [doDownload])
}, [doDownload, outputRef])
useEffect(() => {
if (doImport && editor.current) {
@@ -197,18 +212,32 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
setHighlighting(value)
}
const importFromClipboard = useCallback(async () => {
if (editor.current) {
const text = await navigator.clipboard.readText()
editor.current.setValue(text)
onImport.current()
}
}, [editor, onImport])
return <>
<div class="controls source-controls">
<BtnMenu icon="gear" tooltip={locale('output_settings')} data-cy="source-controls">
{window.matchMedia('(pointer: coarse)').matches && <>
<Btn icon="paste" onClick={importFromClipboard} />
</>}
<Btn label={locale('inline')} active={inline} onClick={() => setInline(!inline)} />
<BtnMenu icon="gear" tooltip={locale('output_settings')}>
{getSourceIndents().map(key =>
<Btn label={locale(`indentation.${key}`)} active={indent === key}
<Btn label={locale(`indentation.${key}`)} active={cIndent === key}
onClick={() => changeIndent(key)}/>
)}
<hr />
{getSourceFormats().map(key =>
<Btn label={locale(`format.${key}`)} active={format === key}
<Btn label={locale(`format.${key}`)} active={cFormat === key}
onClick={() => changeFormat(key)} />)}
<hr />
{/* <Btn icon={sort === 'alphabetically' ? 'square_fill' : 'square'} label={locale('sort_alphabetically')}
onClick={() => setSort(sort === 'alphabetically' ? 'schema' : 'alphabetically')} /> */}
<Btn icon={highlighting ? 'square_fill' : 'square'} label={locale('highlighting')}
onClick={() => changeHighlighting(!highlighting)} />
</BtnMenu>

View File

@@ -1,32 +0,0 @@
import type { DataModel } from '@mcschema/core'
import { useErrorBoundary, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { useModel } from '../../hooks/index.js'
import { FullNode } from '../../schema/renderHtml.js'
import type { BlockStateRegistry, VersionId } from '../../services/index.js'
type TreePanelProps = {
version: VersionId,
model: DataModel | undefined,
blockStates: BlockStateRegistry | undefined,
onError: (message: string) => unknown,
}
export function Tree({ version, model, blockStates, onError }: TreePanelProps) {
const { lang } = useLocale()
if (!model || !blockStates || lang === 'none') return <></>
const [error] = useErrorBoundary(e => {
onError(`Error rendering the tree: ${e.message}`)
console.error(e)
})
if (error) return <></>
const [, setState] = useState(0)
useModel(model, () => {
setState(state => state + 1)
})
return <div class="tree" data-cy="tree">
<FullNode {...{model, lang, version, blockStates}}/>
</div>
}

View File

@@ -1,10 +1,11 @@
export * from './FileCreation.js'
export * from './FileRenaming.js'
export * from './FileView.jsx'
export * from './GeneratorCard.jsx'
export * from './GeneratorList.jsx'
export * from './JsonFileView.jsx'
export * from './PreviewPanel.js'
export * from './ProjectCreation.js'
export * from './ProjectDeletion.js'
export * from './ProjectPanel.js'
export * from './SourcePanel.js'
export * from './Tree.js'

View File

@@ -1,12 +1,11 @@
import { DataModel } from '@mcschema/core'
import { clampedMap } from 'deepslate'
import { mat3 } from 'gl-matrix'
import { useCallback, useRef, useState } from 'preact/hooks'
import { getProjectData, useLocale, useProject, useStore } from '../../contexts/index.js'
import { getWorldgenProjectData, useLocale, useProject, useStore, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/index.js'
import { checkVersion } from '../../services/Schemas.js'
import { checkVersion } from '../../services/Versions.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js'
import { iterateWorld2D, randomSeed, safeJsonParse, stringToColor } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -21,8 +20,9 @@ type Layer = typeof LAYERS[number]
const DETAIL_DELAY = 300
const DETAIL_SCALE = 2
export const BiomeSourcePreview = ({ data, shown, version }: PreviewProps) => {
export const BiomeSourcePreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const { project } = useProject()
const { biomeColors } = useStore()
const [seed, setSeed] = useState(randomSeed())
@@ -31,18 +31,20 @@ export const BiomeSourcePreview = ({ data, shown, version }: PreviewProps) => {
const [focused, setFocused] = useState<string[]>([])
const [focused2, setFocused2] = useState<string[]>([])
const state = JSON.stringify(data)
const text = docAndNode.doc.getText()
const data = safeJsonParse(text) ?? {}
const type: string = data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? ''
const hasRandomness = type === 'multi_noise' || type === 'the_end'
const { value } = useAsync(async function loadBiomeSource() {
await DEEPSLATE.loadVersion(version, getProjectData(project))
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(data?.generator?.settings), DataModel.unwrapLists(data?.generator?.biome_source), seed)
const projectData = await getWorldgenProjectData(project)
await DEEPSLATE.loadVersion(version, projectData)
await DEEPSLATE.loadChunkGenerator(data?.generator?.settings, data?.generator?.biome_source, seed)
return {
biomeSource: { loaded: true },
noiseRouter: checkVersion(version, '1.19') ? DEEPSLATE.getNoiseRouter() : undefined,
}
}, [state, seed, project, version])
}, [text, seed, project, version])
const { biomeSource, noiseRouter } = value ?? {}
const actualLayer = noiseRouter ? layer : 'biomes'

View File

@@ -1,4 +1,3 @@
import { DataModel } from '@mcschema/core'
import { BlockDefinition, Identifier, Structure, StructureRenderer } from 'deepslate/render'
import type { mat4 } from 'gl-matrix'
import { useCallback, useRef } from 'preact/hooks'
@@ -6,19 +5,21 @@ import { useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { AsyncCancel } from '../../hooks/useAsyncFn.js'
import { getResources, ResourceWrapper } from '../../services/Resources.js'
import { safeJsonParse } from '../../Utils.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
const PREVIEW_ID = Identifier.parse('misode:preview')
export const BlockStatePreview = ({ data, shown }: PreviewProps) => {
export const BlockStatePreview = ({ docAndNode, shown }: PreviewProps) => {
const { version } = useVersion()
const serializedData = JSON.stringify(data)
const text = docAndNode.doc.getText()
const { value: resources } = useAsync(async () => {
if (!shown) return AsyncCancel
const resources = await getResources(version)
const definition = BlockDefinition.fromJson(PREVIEW_ID.toString(), DataModel.unwrapLists(data))
const resources = await getResources(version, new Map())
const definition = BlockDefinition.fromJson(safeJsonParse(text) ?? {})
const wrapper = new ResourceWrapper(resources, {
getBlockDefinition(id) {
if (id.equals(PREVIEW_ID)) return definition
@@ -26,7 +27,7 @@ export const BlockStatePreview = ({ data, shown }: PreviewProps) => {
},
})
return wrapper
}, [shown, version, serializedData])
}, [shown, version, text])
const renderer = useRef<StructureRenderer | undefined>(undefined)

View File

@@ -1,9 +1,8 @@
import { DataModel } from '@mcschema/core'
import type { BlockPos, ChunkPos, PerlinNoise, Random } from 'deepslate/worldgen'
import type { Color } from '../../Utils.js'
import { clamp, isObject, stringToColor } from '../../Utils.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion } from '../../services/index.js'
import type { Color } from '../../Utils.js'
import { clamp, isObject, stringToColor } from '../../Utils.js'
export type Placement = [BlockPos, number]
@@ -38,9 +37,9 @@ export const featureColors: Color[] = [
export function decorateChunk(pos: ChunkPos, state: any, ctx: PlacementContext): PlacedFeature[] {
if (checkVersion(ctx.version, undefined, '1.17')) {
getPlacements([pos[0] * 16, 0, pos[1] * 16], DataModel.unwrapLists(state), ctx)
getPlacements([pos[0] * 16, 0, pos[1] * 16], state, ctx)
} else {
modifyPlacement([pos[0] * 16, 0, pos[1] * 16], DataModel.unwrapLists(state.placement), ctx)
modifyPlacement([pos[0] * 16, 0, pos[1] * 16], state.placement, ctx)
}
return ctx.placements.map(([pos, i]) => {
@@ -63,7 +62,9 @@ function decorateY(pos: BlockPos, y: number): BlockPos[] {
}
export function sampleInt(value: any, ctx: PlacementContext): number {
if (typeof value === 'number') {
if (value === undefined) {
return 0
} else if (typeof value === 'number') {
return value
} else if (value.base) {
return value.base ?? 1 + ctx.nextInt(1 + (value.spread ?? 0))
@@ -206,7 +207,7 @@ const Decorators: {
},
count_extra: (config, pos, ctx) => {
let count = config?.count ?? 1
if (ctx.nextFloat() < config.extra_chance ?? 0){
if (ctx.nextFloat() < (config.extra_chance ?? 0)){
count += config.extra_count ?? 0
}
return new Array(count).fill(pos)

View File

@@ -1,18 +1,20 @@
import { BlockPos, ChunkPos, LegacyRandom, PerlinNoise } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { computeIfAbsent, iterateWorld2D, randomSeed } from '../../Utils.js'
import { useLocale, useVersion } from '../../contexts/index.js'
import { computeIfAbsent, iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn } from '../index.js'
import type { PlacedFeature, PlacementContext } from './Decorator.js'
import { decorateChunk } from './Decorator.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
export const DecoratorPreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const [seed, setSeed] = useState(randomSeed())
const state = JSON.stringify(data)
const text = docAndNode.doc.getText()
const { context, chunkFeatures } = useMemo(() => {
const random = new LegacyRandom(seed)
@@ -31,7 +33,7 @@ export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
context,
chunkFeatures: new Map<string, PlacedFeature[]>(),
}
}, [state, version, seed])
}, [text, version, seed])
const ctx = useRef<CanvasRenderingContext2D>()
const imageData = useRef<ImageData>()
@@ -48,7 +50,7 @@ export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
}, [])
const onDraw = useCallback(function onDraw(transform: mat3) {
if (!ctx.current || !imageData.current || !shown) return
const data = safeJsonParse(text) ?? {}
iterateWorld2D(imageData.current, transform, (x, y) => {
const pos = ChunkPos.create(Math.floor(x / 16), Math.floor(-y / 16))
const features = computeIfAbsent(chunkFeatures, `${pos[0]} ${pos[1]}`, () => decorateChunk(pos, data, context))

View File

@@ -1,7 +1,7 @@
import * as deepslate19 from 'deepslate/worldgen'
import { clamp, computeIfAbsent, computeIfAbsentAsync, deepClone, deepEqual, isObject, square } from '../../Utils.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchAllPresets, fetchPreset } from '../../services/index.js'
import { clamp, computeIfAbsent, computeIfAbsentAsync, deepClone, deepEqual, isObject, safeJsonParse, square } from '../../Utils.js'
export type ProjectData = Record<string, Record<string, unknown>>
@@ -131,7 +131,7 @@ export class Deepslate {
const preset = biomeState.preset.replace(/^minecraft:/, '')
const biomes = await computeIfAbsentAsync(this.presetCache, `${version}-${preset}`, async () => {
const dimension = await fetchPreset(version, 'dimension', preset === 'overworld' ? 'overworld' : 'the_nether')
return dimension.generator.biome_source.biomes
return safeJsonParse(dimension)?.generator.biome_source.biomes
})
biomeState = { type: biomeState.type, biomes }
}
@@ -282,7 +282,7 @@ export class Deepslate {
this.settingsCache = settings.noise
const randomState = new this.d.RandomState(settings, seed)
return randomState.router.finalDensity
} else {
} else if (this.isVersion('1.18.2')) {
const random = this.d.XoroshiroRandom.create(seed).forkPositional()
const settings = this.d.NoiseSettings.fromJson({
min_y: minY,
@@ -297,6 +297,8 @@ export class Deepslate {
this.settingsCache = settings
const originalFn = this.d.DensityFunction.fromJson(state)
return originalFn.mapAll(new (this.d.NoiseRouter as any).Visitor(random, settings))
} else {
return undefined
}
}
@@ -315,11 +317,12 @@ export class Deepslate {
finalDensity: this.d.DensityFunction.fromJson(state),
}),
})
const levelHeight: deepslate19.LevelHeight = { minY: 0, height: 256 }
const unknownBiome = this.d.Identifier.create('unknown')
const randomState = new this.d.RandomState(settings, seed)
const biomeSource = new this.d.FixedBiomeSource(unknownBiome)
const chunkGenerator = new this.d.NoiseChunkGenerator(biomeSource, settings)
this.structureContextCache = { seed, settings, randomState, biomeSource, chunkGenerator }
this.structureContextCache = { seed, settings, randomState, biomeSource, chunkGenerator, levelHeight }
class SimpleStructure extends this.d.WorldgenStructure {
constructor(settings: deepslate19.WorldgenStructure.StructureSettings) {

View File

@@ -1,12 +1,12 @@
import { DataModel } from '@mcschema/core'
import type { Voxel } from 'deepslate/render'
import { clampedMap, VoxelRenderer } from 'deepslate/render'
import type { mat3, mat4 } from 'gl-matrix'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { getProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { getWorldgenProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { useLocalStorage } from '../../hooks/useLocalStorage.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -17,25 +17,26 @@ import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
const MODES = ['side', 'top', '3d'] as const
type Mode = typeof MODES[number]
export const DensityFunctionPreview = ({ data, shown }: PreviewProps) => {
export const DensityFunctionPreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { project } = useProject()
const { version } = useVersion()
const [mode, setMode] = useState<Mode>('side')
const [mode, setMode] = useLocalStorage('misode_density_function_mode', 'side')
const voxelMode = mode === '3d'
const topDown = mode === 'top'
const [seed, setSeed] = useState(randomSeed())
const [minY] = useState(0)
const [height] = useState(256)
const serializedData = JSON.stringify(data)
const text = docAndNode.doc.getText()
const { value: df } = useAsync(async () => {
await DEEPSLATE.loadVersion(version, getProjectData(project))
const df = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(data), minY, height, seed)
const projectData = await getWorldgenProjectData(project)
await DEEPSLATE.loadVersion(version, projectData)
const df = DEEPSLATE.loadDensityFunction(safeJsonParse(text) ?? {}, minY, height, seed)
return df
}, [version, project, minY, height, seed, serializedData])
}, [version, project, minY, height, seed, text])
// === 2D ===
const imageData = useRef<ImageData>()
@@ -130,7 +131,7 @@ export const DensityFunctionPreview = ({ data, shown }: PreviewProps) => {
<span>{locale(topDown ? 'y' : 'z')}</span>
<NumberInput value={offset} onChange={setOffset} />
</div>}
<BtnMenu icon="package">
<BtnMenu label={locale(`mode.${mode}`)}>
{MODES.map(m => <Btn label={locale(`mode.${m}`)} active={mode == m} onClick={() => setMode(m)} />)}
</BtnMenu>
<Btn icon="sync" tooltip={locale('generate_new_seed')}

View File

@@ -0,0 +1,272 @@
import { Identifier, ItemStack } from 'deepslate'
import type { ComponentChild, ComponentChildren } from 'preact'
import { useEffect, useRef, useState } from 'preact/hooks'
import { clamp, safeJsonParse } from '../../Utils.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import { TextComponent } from '../TextComponent.jsx'
import type { PreviewProps } from './index.js'
export const DialogPreview = ({ docAndNode }: PreviewProps) => {
const overlay = useRef<HTMLDivElement>(null)
const text = docAndNode.doc.getText()
const dialog = safeJsonParse(text) ?? {}
const type = dialog.type?.replace(/^minecraft:/, '')
const footerHeight = (type === 'dialog_list' || type == 'multi_action' || type == 'server_links') && dialog.exit_action == undefined ? 5 : 33
useEffect(() => {
function resizeHandler() {
if (!overlay.current) return
const width = Math.floor(overlay.current.clientWidth)
overlay.current.style.setProperty('--dialog-px', `${width/400}px`)
}
resizeHandler()
window.addEventListener('resize', resizeHandler)
return () => window.removeEventListener('resize', resizeHandler)
}, [overlay])
return <>
<div ref={overlay} class="preview-overlay dialog-preview" style="--dialog-px: 1px;">
<img src="/images/dialog/background.webp" alt="" draggable={false} />
<div style={'top: 0; left: 0; width: 100%; height: 100%;'}>
<DialogTitle title={dialog.title} />
<div style={`display: flex; flex-direction: column; gap: ${px(10)}; align-items: center; overflow-y: auto; height: calc(100% - ${px(33 + footerHeight)})`}>
<DialogBody body={dialog.body} />
<DialogInputs inputs={dialog.inputs} />
<DialogActions dialog={dialog} />
</div>
<div style={`bottom: 0; left: 0; width: 100%; height: ${px(footerHeight)}; display: flex; justify-content: center; align-items: center;`}>
<DialogFooter dialog={dialog} />
</div>
</div>
</div>
</>
}
function DialogTitle({ title }: { title: any }) {
return <div style={`height: ${px(33)}; display: flex; gap: ${px(10)}; justify-content: center; align-items: center`}>
<TextComponent component={title} />
<WithTooltip tooltip="This is a custom screen. Click here to learn more.">
<div class="dialog-warning-button" style={`width: ${px(20)}; height: ${px(20)};`}></div>
</WithTooltip>
</div>
}
function DialogBody({ body }: { body: any }) {
if (!body) {
body = []
} else if (!Array.isArray(body)) {
body = [body]
}
return <>
{body?.map((b: any) => {
const type = b.type?.replace(/^minecraft:/, '')
if (type === 'plain_message') {
return <div class="dialog-body" style={`max-width: ${px(clamp(b.width ?? 200, 1, 1024))}; padding: ${px(4)}`}>
<TextComponent component={b.contents} />
</div>
}
if (type == 'item') {
// TODO: add item components
const item = new ItemStack(Identifier.parse(b.item?.id ?? 'air'), b.show_decorations ? (b.item?.count ?? 1) : 1)
return <div style={`display: flex; gap: ${px(2)}; align-items: center; gap: ${px(4)}`}>
<div style={`width: ${px(clamp(b.width ?? 16, 1, 256))}; height: ${px(clamp(b.height ?? 16, 1, 256))}`}>
<div style={`width: ${px(16)}; height: ${px(16)}`}>
<ItemDisplay item={item} tooltip={b.show_tooltip ?? true} />
</div>
</div>
{b.description && <div style={`max-width: ${px(clamp(b.description.width ?? 200, 1, 1024))};`}>
<TextComponent component={b.description.contents} />
</div>}
</div>
}
return <></>
})}
</>
}
function DialogInputs({ inputs }: { inputs: any[] | undefined }) {
return <>
{inputs?.map((i: any) => <InputControl input={i} />)}
</>
}
function DialogActions({ dialog }: { dialog: any }) {
const type = dialog.type?.replace(/^minecraft:/, '')
if (type === 'dialog_list') {
let dialogs = []
if (Array.isArray(dialog.dialogs)) {
dialogs = dialog.dialogs
} else if (typeof dialog.dialogs === 'string') {
if (dialog.dialogs.startsWith('#')) {
dialogs = ['dialog_1', 'dialog_2', 'dialog_3']
} else {
dialogs = [dialog.dialogs]
}
}
return <ColumnsGrid columns={dialog.columns ?? 2}>
{dialogs.map((d: any) => {
let text = ''
if (typeof d === 'string') {
text = Identifier.parse(d).path.replaceAll('/', ' ').replaceAll('_', ' ')
text = text.charAt(0).toUpperCase() + text.substring(1)
} else {
text = d?.external_title ?? d?.title ?? ''
}
return <Button label={text} width={dialog.button_width ?? 150} />
})}
</ColumnsGrid>
}
if (type === 'multi_action') {
return <ColumnsGrid columns={dialog.columns ?? 2}>
{dialog.actions?.map((a: any) =>
<Button label={a.label} width={a.width ?? 150} tooltip={a.tooltip} />
) ?? []}
</ColumnsGrid>
}
if (type === 'server_links') {
const links = ['Server link 1', 'Server link 2', 'Server link 3']
return <ColumnsGrid columns={dialog.columns ?? 2}>
{links.map((text: string) => {
return <Button label={text} width={dialog.button_width ?? 150} />
})}
</ColumnsGrid>
}
return <></>
}
function DialogFooter({ dialog }: { dialog: any }) {
const type = dialog.type?.replace(/^minecraft:/, '')
if (type === 'confirmation') {
return <div style={`display: flex; gap: ${px(8)}; justify-content: center;`}>
<Button label={dialog.yes?.label} width={dialog.yes?.width ?? 150} tooltip={dialog.yes?.tooltip} />
<Button label={dialog.no?.label} width={dialog.no?.width ?? 150} tooltip={dialog.no?.tooltip} />
</div>
}
if ((type === 'dialog_list' || type == 'multi_action' || type == 'server_links') && dialog.exit_action) {
return <Button label={dialog.exit_action.label} width={200} />
}
if (type === 'notice') {
return <div style={`display: flex; gap: ${px(8)}; justify-content: center;`}>
<Button label={dialog.action?.label ?? {translate: 'gui.ok'}} width={dialog.action?.width ?? 150} tooltip={dialog.action?.tooltip} />
</div>
}
return <></>
}
function InputControl({ input }: { input: any }) {
const type = input.type?.replace(/^minecraft:/, '')
// TODO: make interactive
if (type === 'boolean') {
return <div style={`display: flex; gap: ${px(4)}; align-items: center;`}>
<div class={`dialog-checkbox ${input.initial ? 'dialog-selected' : ''}`} style={`width: ${px(17)}; height: ${px(17)}`}></div>
<TextComponent component={input.label} base={{color: '#e0e0e0'}} />
</div>
}
if (type === 'number_range') {
const initial = input.initial ?? (((input.start ?? 0) + (input.end ?? 0)) / 2)
const label = {translate: input.label_format ?? 'options.generic_value', with: [input.label ?? '', initial]}
return <div class="dialog-slider" style={`width: ${px(clamp(input.width ?? 200, 1, 1024))}; height: ${px(20)};`}>
<div class="dialog-slider-track"></div>
<div class="dialog-slider-handle"></div>
<div class="dialog-slider-text">
<TextComponent component={label} />
</div>
</div>
}
if (type === 'single_option') {
const initial = input.options?.find((o: any) => o.initial) ?? input.options?.[0]
const initialLabel = typeof initial === 'string' ? initial : initial?.display ?? initial?.id ?? ''
const label = input.label_visible === false ? initialLabel : {translate: 'options.generic_value', with: [input.label ?? '', initialLabel]}
return <Button label={label} width={clamp(input.width ?? 200, 1, 1024)} />
}
if (type === 'text') {
const height = input.multiline
? (input.multiline.height
? clamp(input.multiline.height, 1, 512)
: (9 * Math.max(input.multiline.max_lines ?? 4, 1) + 8))
: 20
return <div style={`display: flex; flex-direction: column; gap: ${px(4)};`}>
{input.label_visible !== false && <TextComponent component={input.label} />}
<div class="dialog-edit-box" style={`width: ${px(clamp(input.width ?? 200, 1, 1024))}; height: ${px(height)};`}>
{input.initial && <TextComponent component={input.initial} />}
</div>
</div>
}
return <></>
}
interface ColumnsGridProps {
columns: number
children: ComponentChild[]
}
function ColumnsGrid({ columns, children }: ColumnsGridProps) {
const totalCount = children.length
const gridCount = Math.floor(totalCount / columns) * columns
return <div style={`padding-top: ${px(4)}; display: grid; grid-template-columns: repeat(${columns}, minmax(0, 1fr)); gap: ${px(2)}; justify-content: center;`}>
{children.slice(0, gridCount)}
{totalCount > gridCount && <div style={`grid-column: span ${columns}; display: flex; gap: ${px(2)}; justify-content: center;`}>
{children.slice(gridCount)}
</div>}
</div>
}
interface ButtonProps {
label: any
width: number
tooltip?: any
}
function Button({ label, width, tooltip }: ButtonProps) {
return <WithTooltip tooltip={tooltip}>
<div class="dialog-button" style={`width: ${px(clamp(width, 1, 1024))}; height: ${px(20)};`}>
<TextComponent component={label} oneline />
</div>
</WithTooltip>
}
interface WithTooltipProps {
tooltip?: any
children: ComponentChildren
}
function WithTooltip({ tooltip, children }: WithTooltipProps) {
if (!tooltip) {
return <>{children}</>
}
const el = useRef<HTMLDivElement>(null)
const [tooltipOffset, setTooltipOffset] = useState<[number, number]>([0, 0])
useEffect(() => {
const onMove = (e: MouseEvent) => {
requestAnimationFrame(() => {
setTooltipOffset([e.offsetX + 20, e.offsetY - 10])
})
}
el.current?.addEventListener('mousemove', onMove)
return () => el.current?.removeEventListener('mousemove', onMove)
}, [])
return <div ref={el} class="tooltip-container">
{children}
{<div class="dialog-tooltip" style={`left: ${tooltipOffset[0]}px; top: ${tooltipOffset[1]}px;`}>
<TextComponent component={tooltip} />
</div>}
</div>
}
function px(n: number) {
return `calc(var(--dialog-px) * ${n})`
}

View File

@@ -0,0 +1,77 @@
import { ItemRenderer, ItemStack, NbtString } from 'deepslate'
import { Identifier, ItemModel } from 'deepslate/render'
import { useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { AsyncCancel } from '../../hooks/useAsyncFn.js'
import { getResources, ResourceWrapper } from '../../services/Resources.js'
import { isObject, safeJsonParse } from '../../Utils.js'
import { ErrorPanel } from '../ErrorPanel.jsx'
import type { PreviewProps } from './index.js'
const PREVIEW_ID = Identifier.parse('misode:preview')
const RENDER_SIZE = 512
export const ItemModelPreview = ({ docAndNode, shown }: PreviewProps) => {
const { version } = useVersion()
const text = docAndNode.doc.getText()
const { value: render, error } = useAsync(async () => {
if (!shown) return AsyncCancel
const resources = await getResources(version, new Map())
const data = safeJsonParse(text) ?? {}
if (!isObject(data) || !isObject(data.model)) {
return undefined
}
const itemModel = ItemModel.fromJson(data.model)
const wrapper = new ResourceWrapper(resources, {
getItemModel(id) {
if (id.equals(PREVIEW_ID)) return itemModel
return null
},
})
const canvas = document.createElement('canvas')
canvas.width = RENDER_SIZE
canvas.height = RENDER_SIZE
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
if (!gl) {
throw new Error('Cannot get WebGL2 context')
}
const item = new ItemStack(PREVIEW_ID, 1, new Map(Object.entries({
'minecraft:item_model': new NbtString(PREVIEW_ID.toString()),
})))
const renderer = new ItemRenderer(gl, item, wrapper, { display_context: 'gui' })
renderer.drawItem()
const url = canvas.toDataURL()
console.log('DRAW', url)
return url
}, [shown, version, text])
if (error) {
return <ErrorPanel error={error} prefix="Failed to initialize preview: " />
}
return <>
<div class="preview-overlay">
<img src="/images/single_item.png" alt="Container background" class="pixelated" draggable={false} />
{render && <div class="flex items-center justify-center" style={slotStyle()}>
<img src={render} class="w-[88.888%]" />
</div>}
</div>
</>
}
const GUI_WIDTH = 176
const GUI_HEIGHT = 81
const SLOT_SIZE = 72
function slotStyle() {
const x = 52
const y = 4
return {
left: `${x*100/GUI_WIDTH}%`,
top: `${y*100/GUI_HEIGHT}%`,
width: `${SLOT_SIZE*100/GUI_WIDTH}%`,
height: `${SLOT_SIZE*100/GUI_HEIGHT}%`,
}
}

View File

@@ -1,15 +1,19 @@
import type { ItemComponentsProvider } from 'deepslate'
import { NbtByte, NbtDouble, NbtLong } from 'deepslate'
import type { Random } from 'deepslate/core'
import { Enchantment, Identifier, ItemStack, LegacyRandom } from 'deepslate/core'
import { NbtCompound, NbtInt, NbtList, NbtShort, NbtString, NbtTag, NbtType } from 'deepslate/nbt'
import { clamp, getWeightedRandom, isObject } from '../../Utils.js'
import type { VersionId } from '../../services/Schemas.js'
import { Identifier, ItemStack, LegacyRandom } from 'deepslate/core'
import { NbtCompound, NbtInt, NbtList, NbtString, NbtTag } from 'deepslate/nbt'
import { ResolvedItem } from '../../services/ResolvedItem.js'
import type { VersionId } from '../../services/Versions.js'
import { checkVersion } from '../../services/Versions.js'
import { clamp, getWeightedRandom, isObject, jsonToNbt } from '../../Utils.js'
export interface SlottedItem {
slot: number,
item: ItemStack,
item: ResolvedItem,
}
type ItemConsumer = (item: ItemStack) => void
type ItemConsumer = (item: ResolvedItem) => void
const StackMixers = {
container: fillContainer,
@@ -18,13 +22,18 @@ const StackMixers = {
type StackMixer = keyof typeof StackMixers
interface LootOptions {
interface LootOptions extends ItemComponentsProvider {
version: VersionId,
seed: bigint,
luck: number,
daytime: number,
weather: string,
stackMixer: StackMixer,
getItemTag(id: string): string[],
getLootTable(id: string): any,
getPredicate(id: string): any,
getEnchantments(): Map<string, any>,
getEnchantmentTag(id: string): string[],
}
interface LootContext extends LootOptions {
@@ -32,14 +41,11 @@ interface LootContext extends LootOptions {
luck: number
weather: string,
dayTime: number,
getItemTag(id: string): string[],
getLootTable(id: string): any,
getPredicate(id: string): any,
}
export function generateLootTable(lootTable: any, options: LootOptions) {
const ctx = createLootContext(options)
const result: ItemStack[] = []
const result: ResolvedItem[] = []
generateTable(lootTable, item => result.push(item), ctx)
const mixer = StackMixers[options.stackMixer]
return mixer(result, ctx)
@@ -47,7 +53,7 @@ export function generateLootTable(lootTable: any, options: LootOptions) {
const SLOT_COUNT = 27
function fillContainer(items: ItemStack[], ctx: LootContext): SlottedItem[] {
function fillContainer(items: ResolvedItem[], ctx: LootContext): SlottedItem[] {
const slots = shuffle([...Array(SLOT_COUNT)].map((_, i) => i), ctx)
const queue = items.filter(i => !i.is('air') && i.count > 1)
@@ -83,7 +89,7 @@ function fillContainer(items: ItemStack[], ctx: LootContext): SlottedItem[] {
return results
}
function assignSlots(items: ItemStack[]): SlottedItem[] {
function assignSlots(items: ResolvedItem[]): SlottedItem[] {
const results: SlottedItem[] = []
let slot = 0
for (const item of items) {
@@ -98,7 +104,7 @@ function assignSlots(items: ItemStack[]): SlottedItem[] {
return results
}
function splitItem(item: ItemStack, count: number): ItemStack {
function splitItem(item: ResolvedItem, count: number): ResolvedItem {
const splitCount = Math.min(count, item.count)
const other = item.clone()
other.count = splitCount
@@ -117,8 +123,11 @@ function shuffle<T>(array: T[], ctx: LootContext) {
}
function generateTable(table: any, consumer: ItemConsumer, ctx: LootContext) {
if (!Array.isArray(table.pools)) {
return
}
const tableConsumer = decorateFunctions(table.functions ?? [], consumer, ctx)
for (const pool of table.pools ?? []) {
for (const pool of table.pools) {
generatePool(pool, tableConsumer, ctx)
}
}
@@ -130,9 +139,6 @@ function createLootContext(options: LootOptions): LootContext {
luck: options.luck,
weather: options.weather,
dayTime: options.daytime,
getItemTag: () => [],
getLootTable: () => ({ pools: [] }),
getPredicate: () => [],
}
}
@@ -203,7 +209,7 @@ function expandEntry(entry: any, ctx: LootContext, consumer: (entry: any) => voi
return true
case 'tag':
if (entry.expand) {
ctx.getItemTag(entry.tag ?? '').forEach(tagEntry => {
ctx.getItemTag(entry.name ?? '').forEach(tagEntry => {
consumer({ type: 'item', name: tagEntry })
})
} else {
@@ -229,19 +235,20 @@ function createItem(entry: any, consumer: ItemConsumer, ctx: LootContext) {
}
switch (type) {
case 'item':
try {
entryConsumer(new ItemStack(Identifier.parse(entry.name), 1))
} catch (e) {}
const id = Identifier.parse(entry.name)
entryConsumer(new ResolvedItem(new ItemStack(id, 1), ctx.getItemComponents(id)))
break
case 'tag':
ctx.getItemTag(entry.name).forEach(tagEntry => {
try {
entryConsumer(new ItemStack(Identifier.parse(tagEntry), 1))
} catch (e) {}
const id = Identifier.parse(tagEntry)
entryConsumer(new ResolvedItem(new ItemStack(id, 1), ctx.getItemComponents(id)))
})
break
case 'loot_table':
generateTable(ctx.getLootTable(entry.name), entryConsumer, ctx)
const lootTable = typeof entry.value === 'string' ? ctx.getLootTable(entry.value) : entry.value
if (lootTable !== undefined) {
generateTable(lootTable, entryConsumer, ctx)
}
break
case 'dynamic':
// not relevant for this simulation
@@ -253,7 +260,7 @@ function computeWeight(entry: any, luck: number) {
return Math.max(Math.floor((entry.weight ?? 1) + (entry.quality ?? 0) * luck), 0)
}
type LootFunction = (item: ItemStack, ctx: LootContext) => void
type LootFunction = (item: ResolvedItem, ctx: LootContext) => void
function decorateFunctions(functions: any[], consumer: ItemConsumer, ctx: LootContext): ItemConsumer {
const compositeFunction = composeFunctions(functions)
@@ -268,7 +275,7 @@ function composeFunctions(functions: any[]): LootFunction {
for (const fn of functions) {
if (Array.isArray(fn)) {
composeFunctions(fn)
} else if (composeConditions(fn.conditions ?? [])(ctx)) {
} else if (isObject(fn) && composeConditions(fn.conditions ?? [])(ctx)) {
const type = fn.function?.replace(/^minecraft:/, '');
(LootFunctions[type]?.(fn) ?? (i => i))(item, ctx)
}
@@ -277,44 +284,47 @@ function composeFunctions(functions: any[]): LootFunction {
}
const LootFunctions: Record<string, (params: any) => LootFunction> = {
enchant_randomly: ({ enchantments }) => (item, ctx) => {
const isBook = item.is('book')
if (enchantments === undefined || enchantments.length === 0) {
enchantments = Enchantment.REGISTRY.map((_, ench) => ench)
.filter(ench => ench.isDiscoverable && (isBook || Enchantment.canEnchant(item, ench)))
.map(e => e.id.toString())
enchant_randomly: ({ options, only_compatible }) => (item, ctx) => {
let enchantments = options
? getHomogeneousList(options, ctx.getEnchantmentTag)
: [...ctx.getEnchantments().keys()]
if (!item.is('book') && (only_compatible ?? true)) {
enchantments = enchantments.filter(e => {
const ench = ctx.getEnchantments().get(e.replace(/^minecraft:/, ''))
if (!ench) return true
const supportedItems = getHomogeneousList(ench.supported_items, ctx.getItemTag)
return supportedItems.some(i => item.is(i))
})
}
if (enchantments.length > 0) {
const id = enchantments[ctx.random.nextInt(enchantments.length)]
let ench: Enchantment | undefined
try {
ench = Enchantment.REGISTRY.get(Identifier.parse(id))
} catch (e) {}
if (ench === undefined) return
const lvl = ctx.random.nextInt(ench.maxLevel - ench.minLevel + 1) + ench.minLevel
if (isBook) {
item.tag = new NbtCompound()
item.count = 1
}
enchantItem(item, { id, lvl })
if (isBook) {
item.id = Identifier.create('enchanted_book')
}
if (enchantments.length === 0) {
return
}
},
enchant_with_levels: ({ levels, treasure }) => (item, ctx) => {
const enchants = selectEnchantments(ctx.random, item, computeInt(levels, ctx), treasure)
const isBook = item.is('book')
if (isBook) {
item.count = 1
item.tag = new NbtCompound()
}
for (const enchant of enchants) {
enchantItem(item, enchant)
}
if (isBook) {
const pick = enchantments[ctx.random.nextInt(enchantments.length)]
const maxLevel = ctx.getEnchantments().get(pick.replace(/^minecraft:/, ''))?.max_level ?? 1
const level = ctx.random.nextInt(maxLevel - 1) + 1
if (item.is('book')) {
item.id = Identifier.create('enchanted_book')
item.base = ctx.getItemComponents(item.id)
}
updateEnchantments(item, levels => {
return levels.set(Identifier.parse(pick).toString(), level)
})
},
enchant_with_levels: ({ options, levels }) => (item, ctx) => {
const allowed = options
? getHomogeneousList(options, ctx.getEnchantmentTag)
: [...ctx.getEnchantments().keys()]
const selected = selectEnchantments(item, computeInt(levels, ctx), allowed, ctx)
if (item.is('book')) {
item.id = Identifier.create('enchanted_book')
item.base = ctx.getItemComponents(item.id)
}
updateEnchantments(item, levelsMap => {
for (const { id, lvl } of selected) {
levelsMap.set(id.toString(), lvl)
}
return levelsMap
})
},
exploration_map: ({ decoration }) => (item) => {
if (!item.is('map')) {
@@ -323,64 +333,185 @@ const LootFunctions: Record<string, (params: any) => LootFunction> = {
item.id = Identifier.create('filled_map')
const color = decoration === 'mansion' ? 5393476 : decoration === 'monument' ? 3830373 : -1
if (color >= 0) {
getOrCreateTag(item, 'display').set('MapColor', new NbtInt(color))
item.set('map_color', new NbtInt(color))
}
},
filtered: ({ item_filter, modifier }) => (item, ctx) => {
if (testItemPredicate(item_filter, item, ctx)) {
composeFunctions([modifier])(item, ctx)
}
},
limit_count: ({ limit }) => (item, ctx) => {
const { min, max } = prepareIntRange(limit, ctx)
item.count = clamp(item.count, min, max )
item.count = clamp(item.count, min, max)
},
sequence: ({ functions }) => (item, ctx) => {
if (!Array.isArray(functions)) return
composeFunctions(functions)(item, ctx)
},
set_count: ({ count, add }) => (item, ctx) => {
const oldCount = add ? (item.count) : 0
item.count = clamp(oldCount + computeInt(count, ctx), 0, 64)
},
set_damage: ({ damage, add }) => (item, ctx) => {
const maxDamage = item.getItem().durability
if (maxDamage) {
const oldDamage = add ? 1 - item.tag.getNumber('Damage') / maxDamage : 0
const newDamage = 1 - clamp(computeFloat(damage, ctx) + oldDamage, 0, 1)
const finalDamage = Math.floor(newDamage * maxDamage)
item.tag.set('Damage', new NbtInt(finalDamage))
}
},
set_enchantments: ({ enchantments, add }) => (item, ctx) => {
Object.entries(enchantments).forEach(([id, level]) => {
const lvl = computeInt(level, ctx)
try {
enchantItem(item, { id: Identifier.parse(id), lvl }, add)
} catch (e) {}
set_attributes: ({ modifiers, replace }) => (item, ctx) => {
if (!Array.isArray(modifiers)) return
const newModifiers = modifiers.map<AttributeModifier>(m => {
if (!isObject(m)) m = {}
return {
id: Identifier.parse(typeof m.id === 'string' ? m.id : ''),
type: Identifier.parse(typeof m.attribute === 'string' ? m.attribute : ''),
amount: computeFloat(m.amount, ctx),
operation: typeof m.operation === 'string' ? m.operation : 'add_value',
slot: typeof m.slot === 'string' ? m.slot : Array.isArray(m.slot) ? m.slot[ctx.random.nextInt(m.slot.length)] : 'any',
}
})
updateAttributes(item, (modifiers) => {
if (replace === false) {
return [...modifiers, ...newModifiers]
} else {
return newModifiers
}
})
},
set_lore: ({ lore, replace }) => (item) => {
const lines: string[] = lore.flatMap((line: any) => line !== undefined ? [JSON.stringify(line)] : [])
const newLore = replace ? lines : [...item.tag.getCompound('display').getList('Lore', NbtType.String).map(s => s.getAsString()), ...lines]
getOrCreateTag(item, 'display').set('Lore', new NbtList(newLore.map(l => new NbtString(l))))
},
set_name: ({ name }) => (item) => {
if (name !== undefined) {
const newName = JSON.stringify(name)
getOrCreateTag(item, 'display').set('Name', new NbtString(newName))
set_banner_pattern: ({ patterns, append }) => (item) => {
if (!Array.isArray(patterns)) return
if (append) {
const existing = item.get('banner_patterns', tag => tag.isList() ? tag : undefined) ?? new NbtList()
item.set('banner_patterns', new NbtList([...existing.getItems(), ...patterns.map(jsonToNbt)]))
} else {
item.set('banner_patterns', jsonToNbt(patterns))
}
},
set_nbt: ({ tag }) => (item) => {
set_book_cover: ({ title, author, generation }) => (item) => {
const content = item.get('written_book_content', tag => tag.isCompound() ? tag : undefined) ?? new NbtCompound()
const newContent = new NbtCompound()
.set('title', title !== undefined ? jsonToNbt(title) : content.get('title') ?? new NbtString(''))
.set('author', author !== undefined ? jsonToNbt(author) : content.get('author') ?? new NbtString(''))
.set('generation', generation !== undefined ? jsonToNbt(generation) : content.get('generation') ?? new NbtInt(0))
.set('pages', content.getList('pages'))
.set('resolved', content.get('resolved') ?? new NbtByte(1))
item.set('written_book_content', newContent)
},
set_components: ({ components }) => (item) => {
if (!isObject(components)) {
return
}
for (const [key, value] of Object.entries(components)) {
item.set(key, jsonToNbt(value))
}
},
set_contents: ({ component, entries }) => (item, ctx) => {
if (typeof component !== 'string' || !Array.isArray(entries)) {
return
}
const result = generateLootTable({ pools: [{ rolls: 1, entries: entries }] }, ctx)
if (Identifier.parse(component).is('container')) {
item.set(component, new NbtList(result.map(s => new NbtCompound()
.set('slot', new NbtInt(s.slot))
.set('item', s.item.toNbt())
)))
} else {
item.set(component, new NbtList(result.map(s => s.item.toNbt())))
}
},
set_count: ({ count, add }) => (item, ctx) => {
const oldCount = add ? (item.count) : 0
item.count = oldCount + computeInt(count, ctx)
},
set_custom_data: ({ tag }) => (item) => {
try {
const newTag = NbtTag.fromString(tag)
if (newTag.isCompound()) {
item.tag = newTag
item.set('custom_data', newTag)
}
} catch (e) {}
},
set_custom_model_data: ({ value }) => (item, ctx) => {
item.set('custom_model_data', new NbtInt(computeInt(value, ctx)))
},
set_damage: ({ damage, add }) => (item, ctx) => {
if (item.isDamageable()) {
const maxDamage = item.getMaxDamage()
const oldDamage = add ? 1 - item.getDamage() / maxDamage : 0
const newDamage = 1 - clamp(computeFloat(damage, ctx) + oldDamage, 0, 1)
const finalDamage = Math.floor(newDamage * maxDamage)
item.set('damage', new NbtInt(clamp(finalDamage, 0, maxDamage)))
}
},
set_enchantments: ({ enchantments, add }) => (item, ctx) => {
if (!isObject(enchantments)) {
return
}
if (item.is('book')) {
item.id = Identifier.create('enchanted_book')
item.base = ctx.getItemComponents(item.id)
}
updateEnchantments(item, levels => {
Object.entries(enchantments).forEach(([id, level]) => {
id = Identifier.parse(id).toString()
if (add) {
levels.set(id, clamp((levels.get(id) ?? 0) + computeInt(level, ctx), 0, 255))
} else {
levels.set(id, clamp(computeInt(level, ctx), 0, 255))
}
})
return levels
})
},
set_firework_explosion: () => () => {
// TODO
},
set_fireworks: () => () => {
// TODO
},
set_instrument: () => () => {
// TODO: depends on item tag
},
set_item: ({ item: newId }) => (item, ctx) => {
if (typeof newId !== 'string') return
item.id = Identifier.parse(newId)
item.base = ctx.getItemComponents(item.id)
},
set_loot_table: ({ name, seed }) => (item) => {
item.set('container_loot', new NbtCompound()
.set('loot_table', new NbtString(Identifier.parse(typeof name === 'string' ? name : '').toString()))
.set('seed', new NbtLong(typeof seed === 'number' ? BigInt(seed) : BigInt(0))))
},
set_lore: ({ lore }) => (item, ctx) => {
if (!Array.isArray(lore)) return
const lines: NbtTag[] = lore.flatMap((line: any) => line !== undefined ? [
!checkVersion(ctx.version, '1.21.5')
? new NbtString(JSON.stringify(line))
: jsonToNbt(line),
] : [])
// TODO: account for mode
item.set('lore', new NbtList(lines))
},
set_name: ({ name, target }) => (item, ctx) => {
if (name !== undefined) {
const newName = !checkVersion(ctx.version, '1.21.5')
? new NbtString(JSON.stringify(name))
: jsonToNbt(name)
item.set(target ?? 'custom_name', newName)
}
},
set_ominous_bottle_amplifier: ({ amplifier }) => (item, ctx) => {
item.set('ominous_bottle_amplifier', new NbtInt(computeInt(amplifier, ctx)))
},
set_potion: ({ id }) => (item) => {
if (typeof id === 'string') {
try {
item.tag.set('Potion', new NbtString(Identifier.parse(id).toString()))
} catch (e) {}
item.set('potion_contents', new NbtString(id))
}
},
toggle_tooltips: ({ toggles }) => (item) => {
if (!isObject(toggles)) {
return
}
Object.entries(toggles).forEach(([key, value]) => {
if (typeof value !== 'boolean') return
const tag = item.get(key, tag => tag)
if (tag === undefined) return
if (tag.isCompound()) {
item.set(key, tag.set('show_in_tooltip', new NbtByte(value)))
}
})
},
}
type LootCondition = (ctx: LootContext) => boolean
@@ -400,6 +531,9 @@ function testCondition(condition: any, ctx: LootContext): boolean {
if (Array.isArray(condition)) {
return composeConditions(condition)(ctx)
}
if (!isObject(condition) || typeof condition.condition !== 'string') {
return false
}
const type = condition.condition?.replace(/^minecraft:/, '')
return (LootConditions[type]?.(condition) ?? (() => true))(ctx)
}
@@ -427,14 +561,14 @@ const LootConditions: Record<string, (params: any) => LootCondition> = {
block_state_property: () => () => {
return false // TODO
},
damage_source_properties: ({ predicate }) => (ctx) => {
return testDamageSourcePredicate(predicate, ctx)
damage_source_properties: () => () => {
return false // TODO
},
entity_properties: ({ predicate }) => (ctx) => {
return testEntityPredicate(predicate, ctx)
entity_properties: () => () => {
return false // TODO
},
entity_scores: () => () => {
return false // TODO,
return false // TODO
},
inverted: ({ term }) => (ctx) => {
return !testCondition(term, ctx)
@@ -442,11 +576,11 @@ const LootConditions: Record<string, (params: any) => LootCondition> = {
killed_by_player: ({ inverted }) => () => {
return (inverted ?? false) === false // TODO
},
location_check: ({ predicate }) => (ctx) => {
return testLocationPredicate(predicate, ctx)
location_check: () => () => {
return false // TODO
},
match_tool: ({ predicate }) => (ctx) => {
return testItemPredicate(predicate, ctx)
match_tool: () => () => {
return false // TODO
},
random_chance: ({ chance }) => (ctx) => {
return ctx.random.nextFloat() < chance
@@ -466,6 +600,9 @@ const LootConditions: Record<string, (params: any) => LootCondition> = {
},
survives_explosion: () => () => true,
table_bonus: ({ chances }) => (ctx) => {
if (!chances) {
return false
}
const level = 0 // TODO: get enchantment level from tool
const chance = chances[clamp(level, 0, chances.length - 1)]
return ctx.random.nextFloat() < chance
@@ -511,7 +648,13 @@ function computeInt(provider: any, ctx: LootContext): number {
result += 1
}
}
return result
return result
case 'sum':
let sum = 0
for (const summand of provider.summands ?? []) {
sum += computeInt(summand, ctx)
}
return sum
}
return 0
}
@@ -537,7 +680,13 @@ function computeFloat(provider: any, ctx: LootContext): number {
result += 1
}
}
return result
return result
case 'sum':
let sum = 0
for (const summand of provider.summands ?? []) {
sum += computeFloat(summand, ctx)
}
return sum
}
return 0
}
@@ -546,140 +695,199 @@ function prepareIntRange(range: any, ctx: LootContext) {
if (typeof range === 'number') {
range = { min: range, max: range }
}
const min = computeInt(range.min, ctx)
const max = computeInt(range.max, ctx)
const min = computeInt(range?.min, ctx)
const max = computeInt(range?.max, ctx)
return { min, max }
}
function testItemPredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function testLocationPredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function testEntityPredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function testDamageSourcePredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function enchantItem(item: ItemStack, enchant: Enchant, additive?: boolean) {
const listKey = item.is('book') ? 'StoredEnchantments' : 'Enchantments'
if (!item.tag.hasList(listKey, NbtType.Compound)) {
item.tag.set(listKey, new NbtList())
}
const enchantments = item.tag.getList(listKey, NbtType.Compound).getItems()
let index = enchantments.findIndex((e: any) => e.id === enchant.id)
if (index !== -1) {
const oldEnch = enchantments[index]
oldEnch.set('lvl', new NbtShort(Math.max(additive ? oldEnch.getNumber('lvl') + enchant.lvl : enchant.lvl, 0)))
} else {
enchantments.push(new NbtCompound().set('id', new NbtString(enchant.id.toString())).set('lvl', new NbtShort(enchant.lvl)))
index = enchantments.length - 1
}
if (enchantments[index].getNumber('lvl') === 0) {
enchantments.splice(index, 1)
}
item.tag.set(listKey, new NbtList(enchantments))
}
function selectEnchantments(random: Random, item: ItemStack, levels: number, treasure: boolean): Enchant[] {
const enchantmentValue = item.getItem().enchantmentValue
if (enchantmentValue === undefined) {
return []
}
levels += 1 + random.nextInt(Math.floor(enchantmentValue / 4 + 1)) + random.nextInt(Math.floor(enchantmentValue / 4 + 1))
const f = (random.nextFloat() + random.nextFloat() - 1) * 0.15
levels = clamp(Math.round(levels + levels * f), 1, Number.MAX_SAFE_INTEGER)
let available = getAvailableEnchantments(item, levels, treasure)
if (available.length === 0) {
return []
}
const result: Enchant[] = []
const first = getWeightedRandom(random, available, getEnchantWeight)
if (first) result.push(first)
while (random.nextInt(50) <= levels) {
if (result.length > 0) {
const lastAdded = result[result.length - 1]
available = available.filter(a => Enchantment.isCompatible(Enchantment.REGISTRY.getOrThrow(a.id), Enchantment.REGISTRY.getOrThrow(lastAdded.id)))
function getHomogeneousList(value: unknown, tagGetter: (id: string) => string[]): string[] {
if (typeof value === 'string') {
if (value.startsWith('#')) {
return [...new Set(tagGetter(value.slice(1)).flatMap(e => getHomogeneousList(e, tagGetter)))]
} else {
return [value]
}
if (available.length === 0) break
const ench = getWeightedRandom(random, available, getEnchantWeight)
if (ench) result.push(ench)
levels = Math.floor(levels / 2)
}
return result
if (Array.isArray(value)) {
return value
}
return []
}
const EnchantmentsRarityWeights = new Map(Object.entries<number>({
common: 10,
uncommon: 5,
rare: 2,
very_rare: 1,
}))
function getEnchantWeight(ench: Enchant) {
return EnchantmentsRarityWeights.get(Enchantment.REGISTRY.get(ench.id)?.rarity ?? 'common') ?? 10
}
function getAvailableEnchantments(item: ItemStack, levels: number, treasure: boolean): Enchant[] {
const result: Enchant[] = []
const isBook = item.is('book')
Enchantment.REGISTRY.forEach((id, ench) => {
if ((!ench.isTreasure || treasure) && ench.isDiscoverable && (Enchantment.canEnchant(item, ench) || isBook)) {
for (let lvl = ench.maxLevel; lvl > ench.minLevel - 1; lvl -= 1) {
if (levels >= ench.minCost(lvl) && levels <= ench.maxCost(lvl)) {
result.push({ id, lvl })
}
function testItemPredicate(predicate: any, item: ResolvedItem, ctx: LootContext) {
if (!isObject(predicate)) return false
if (predicate.items !== undefined) {
const allowedItems = getHomogeneousList(predicate.items, ctx.getItemTag)
if (!allowedItems.some(i => item.id.is(i))) {
return false
}
}
if (predicate.count !== undefined) {
const { min, max } = prepareIntRange(predicate.count, ctx)
if (min > item.count || item.count > max) {
return false
}
}
if (isObject(predicate.components)) {
for (const [key, value] of Object.entries(predicate.components)) {
const tag = jsonToNbt(value)
const other = item.get(key, tag => tag)
if (!other || !tag.equals(other)) {
return false
}
}
}
// TODO: item sub predicates
return true
}
function updateEnchantments(item: ResolvedItem, fn: (levels: Map<string, number>) => Map<string, number>) {
const type = item.is('book') ? 'stored_enchantments' : 'enchantments'
if (!item.has(type)) {
return
}
const levelsTag = item.get(type, tag => {
return tag.isCompound() ? tag.has('levels') ? tag.getCompound('levels') : tag : undefined
}) ?? new NbtCompound()
const showInTooltip = item.get(type, tag => {
return tag.isCompound() && tag.hasCompound('levels') ? tag.get('show_in_tooltip') : undefined
}) ?? new NbtByte(1)
const levels = new Map<string, number>()
levelsTag.forEach((id, lvl) => {
levels.set(Identifier.parse(id).toString(), lvl.getAsNumber())
})
return result
const newLevels = fn(levels)
const newLevelsTag = new NbtCompound()
for (const [key, lvl] of newLevels) {
if (lvl > 0) {
newLevelsTag.set(key, new NbtInt(lvl))
}
}
const newTag = new NbtCompound()
.set('levels', newLevelsTag)
.set('show_in_tooltip', showInTooltip)
item.set(type, newTag)
}
interface AttributeModifier {
id: Identifier,
type: Identifier,
amount: number,
operation: string,
slot: string,
}
function updateAttributes(item: ResolvedItem, fn: (modifiers: AttributeModifier[]) => AttributeModifier[]) {
const modifiersTag = item.get('attribute_modifiers', tag => {
return tag.isCompound() ? tag.getList('modifiers') : tag.isList() ? tag : undefined
}) ?? new NbtList()
const showInTooltip = item.get('attribute_modifiers', tag => {
return tag.isCompound() ? tag.get('show_in_tooltip') : undefined
}) ?? new NbtByte(1)
const modifiers = modifiersTag.map<AttributeModifier>(m => {
const root = m.isCompound() ? m : new NbtCompound()
return {
id: Identifier.parse(root.getString('id')),
type: Identifier.parse(root.getString('type')),
amount: root.getNumber('amount'),
operation: root.getString('operation'),
slot: root.getString('slot'),
}
})
const newModifiers = fn(modifiers)
const newModifiersTag = new NbtList(newModifiers.map(m => {
return new NbtCompound()
.set('id', new NbtString(m.id.toString()))
.set('type', new NbtString(m.type.toString()))
.set('amount', new NbtDouble(m.amount))
.set('operation', new NbtString(m.operation))
.set('slot', new NbtString(m.slot))
}))
const newTag = new NbtCompound()
.set('modifiers', newModifiersTag)
.set('show_in_tooltip', showInTooltip)
item.set('attribute_modifiers', newTag)
}
interface Enchant {
id: Identifier,
lvl: number,
id: Identifier
lvl: number
}
const AlwaysHasGlint = new Set([
'minecraft:debug_stick',
'minecraft:enchanted_golden_apple',
'minecraft:enchanted_book',
'minecraft:end_crystal',
'minecraft:experience_bottle',
'minecraft:written_book',
])
function selectEnchantments(item: ResolvedItem, levels: number, options: string[], ctx: LootContext): Enchant[] {
let enchantable: number | undefined = 1 // Not fully correct before version 1.21.2
if (checkVersion(ctx.version, '1.21.2')) {
enchantable = item.get('enchantable', tag => tag.isCompound() ? tag.getNumber('value') : undefined)
if (enchantable === undefined) {
return []
}
}
let cost = levels + 1 + ctx.random.nextInt(Math.floor(enchantable / 4 + 1)) + ctx.random.nextInt(Math.floor(enchantable / 4 + 1))
const f = (ctx.random.nextFloat() + ctx.random.nextFloat() - 1) * 0.15
cost = clamp(Math.round(cost + cost * f), 1, Number.MAX_SAFE_INTEGER)
let available = getAvailableEnchantments(item, cost, options, ctx)
if (available.length === 0) {
return []
}
function getEnchantWeight(ench: Enchant): number {
return ctx.getEnchantments().get(ench.id.toString().replace(/^minecraft:/, ''))?.weight ?? 0
}
const result: Enchant[] = []
const first = getWeightedRandom(ctx.random, available, getEnchantWeight)
if (first) result.push(first)
export function itemHasGlint(item: ItemStack) {
if (AlwaysHasGlint.has(item.id.toString())) {
return true
while (ctx.random.nextInt(50) <= cost) {
if (result.length > 0) {
const lastAdded = result[result.length - 1]
available = available.filter(a => areCompatibleEnchantments(a, lastAdded, ctx))
}
if (available.length === 0) break
const ench = getWeightedRandom(ctx.random, available, getEnchantWeight)
if (ench) result.push(ench)
cost = Math.floor(cost / 2)
}
if (item.is('compass') && (item.tag.has('LodestoneDimension') || item.tag.has('LodestonePos'))) {
return true
}
if ((item.is('potion') || item.is('splash_potion') || item.is('lingering_potion')) && (item.tag.has('Potion') || item.tag.has('CustomPotionEffects'))) {
return true
}
if (item.tag.getList('Enchantments').length > 0 || item.tag.getList('StoredEnchantments').length > 0) {
return true
}
return false
return result
}
function getOrCreateTag(item: ItemStack, key: string) {
if (item.tag.hasCompound(key)) {
return item.tag.getCompound(key)
} else {
const tag = new NbtCompound()
item.tag.set(key, tag)
return tag
function getAvailableEnchantments(item: ResolvedItem, cost: number, options: string[], ctx: LootContext): Enchant[] {
const result: Enchant[] = []
for (const id of options) {
const ench = ctx.getEnchantments().get(id.replace(/^minecraft:/, ''))
if (ench === undefined) continue
const primaryItems = getHomogeneousList(ench.primary_items ?? ench.supported_items, ctx.getItemTag)
if (item.is('book') || primaryItems.some((i: string) => item.id.is(i))) {
for (let lvl = ench.max_level; lvl > 0; lvl -= 1) {
if (cost >= enchantmentCost(ench.min_cost, lvl) && cost <= enchantmentCost(ench.max_cost, lvl)) {
result.push({ id: Identifier.parse(id), lvl })
}
}
}
}
return result
}
function enchantmentCost(value: any, level: number): number {
return value.base + value.per_level_above_first * (level - 1)
}
function areCompatibleEnchantments(a: Enchant, b: Enchant, ctx: LootContext) {
if (a.id.equals(b.id)) {
return false
}
const enchA = ctx.getEnchantments().get(a.id.toString().replace(/^minecraft:/, ''))
const exclusiveA = getHomogeneousList(enchA?.exclusive_set ?? [], ctx.getEnchantmentTag)
if (exclusiveA.some(id => b.id.is(id))) {
return false
}
const enchB = ctx.getEnchantments().get(b.id.toString().replace(/^minecraft:/, ''))
const exclusiveB = getHomogeneousList(enchB?.exclusive_set ?? [], ctx.getEnchantmentTag)
if (exclusiveB.some(id => a.id.is(id))) {
return false
}
return true
}

View File

@@ -0,0 +1,698 @@
import type { Random } from 'deepslate-1.20.4/core'
import { Enchantment, Identifier, ItemStack, LegacyRandom } from 'deepslate-1.20.4/core'
import { NbtCompound, NbtInt, NbtList, NbtShort, NbtString, NbtTag, NbtType } from 'deepslate-1.20.4/nbt'
import type { VersionId } from '../../services/Versions.js'
import { clamp, getWeightedRandom, isObject } from '../../Utils.js'
export interface SlottedItem {
slot: number,
item: ItemStack,
}
type ItemConsumer = (item: ItemStack) => void
const StackMixers = {
container: fillContainer,
default: assignSlots,
}
type StackMixer = keyof typeof StackMixers
interface LootOptions {
version: VersionId,
seed: bigint,
luck: number,
daytime: number,
weather: string,
stackMixer: StackMixer,
getItemTag(id: string): string[],
getLootTable(id: string): any,
getPredicate(id: string): any,
}
interface LootContext extends LootOptions {
random: Random,
luck: number
weather: string,
dayTime: number,
}
export function generateLootTable(lootTable: any, options: LootOptions) {
const ctx = createLootContext(options)
const result: ItemStack[] = []
generateTable(lootTable, item => result.push(item), ctx)
const mixer = StackMixers[options.stackMixer]
return mixer(result, ctx)
}
const SLOT_COUNT = 27
function fillContainer(items: ItemStack[], ctx: LootContext): SlottedItem[] {
const slots = shuffle([...Array(SLOT_COUNT)].map((_, i) => i), ctx)
const queue = items.filter(i => !i.is('air') && i.count > 1)
items = items.filter(i => !i.is('air') && i.count === 1)
while (SLOT_COUNT - items.length - queue.length > 0 && queue.length > 0) {
const [itemA] = queue.splice(ctx.random.nextInt(queue.length), 1)
const splitCount = ctx.random.nextInt(Math.floor(itemA.count / 2)) + 1
const itemB = splitItem(itemA, splitCount)
for (const item of [itemA, itemB]) {
if (item.count > 1 && ctx.random.nextFloat() < 0.5) {
queue.push(item)
} else {
items.push(item)
}
}
}
items.push(...queue)
shuffle(items, ctx)
const results: SlottedItem[] = []
for (const item of items) {
const slot = slots.pop()
if (slot === undefined) {
break
}
if (!item.is('air') && item.count > 0) {
results.push({ slot, item })
}
}
return results
}
function assignSlots(items: ItemStack[]): SlottedItem[] {
const results: SlottedItem[] = []
let slot = 0
for (const item of items) {
if (slot >= 27) {
break
}
if (!item.is('air') && item.count > 0) {
results.push({ slot, item })
slot += 1
}
}
return results
}
function splitItem(item: ItemStack, count: number): ItemStack {
const splitCount = Math.min(count, item.count)
const other = item.clone()
other.count = splitCount
item.count = item.count - splitCount
return other
}
function shuffle<T>(array: T[], ctx: LootContext) {
let i = array.length
while (i > 0) {
const j = ctx.random.nextInt(i)
i -= 1;
[array[i], array[j]] = [array[j], array[i]]
}
return array
}
function generateTable(table: any, consumer: ItemConsumer, ctx: LootContext) {
if (!Array.isArray(table.pools)) {
return
}
const tableConsumer = decorateFunctions(table.functions ?? [], consumer, ctx)
for (const pool of table.pools) {
generatePool(pool, tableConsumer, ctx)
}
}
function createLootContext(options: LootOptions): LootContext {
return {
...options,
random: new LegacyRandom(options.seed),
luck: options.luck,
weather: options.weather,
dayTime: options.daytime,
}
}
function generatePool(pool: any, consumer: ItemConsumer, ctx: LootContext) {
if (composeConditions(pool.conditions ?? [])(ctx)) {
const poolConsumer = decorateFunctions(pool.functions ?? [], consumer, ctx)
const rolls = computeInt(pool.rolls, ctx) + Math.floor(computeFloat(pool.bonus_rolls, ctx) * ctx.luck)
for (let i = 0; i < rolls; i += 1) {
let totalWeight = 0
const entries: any[] = []
// Expand entries
for (const entry of pool.entries ?? []) {
expandEntry(entry, ctx, (e) => {
const weight = computeWeight(e, ctx.luck)
if (weight > 0) {
entries.push(e)
totalWeight += weight
}
})
}
// Select random entry
if (totalWeight === 0 || entries.length === 0) {
continue
}
if (entries.length === 1) {
createItem(entries[0], poolConsumer, ctx)
continue
}
let remainingWeight = ctx.random.nextInt(totalWeight)
for (const entry of entries) {
remainingWeight -= computeWeight(entry, ctx.luck)
if (remainingWeight < 0) {
createItem(entry, poolConsumer, ctx)
break
}
}
}
}
}
function expandEntry(entry: any, ctx: LootContext, consumer: (entry: any) => void): boolean {
if (!canEntryRun(entry, ctx)) {
return false
}
const type = entry.type?.replace(/^minecraft:/, '')
switch (type) {
case 'group':
for (const child of entry.children ?? []) {
expandEntry(child, ctx, consumer)
}
return true
case 'alternatives':
for (const child of entry.children ?? []) {
if (expandEntry(child, ctx, consumer)) {
return true
}
}
return false
case 'sequence':
for (const child of entry.children ?? []) {
if (!expandEntry(child, ctx, consumer)) {
return false
}
}
return true
case 'tag':
if (entry.expand) {
ctx.getItemTag(entry.name ?? '').forEach(tagEntry => {
consumer({ type: 'item', name: tagEntry })
})
} else {
consumer(entry)
}
return true
default:
consumer(entry)
return true
}
}
function canEntryRun(entry: any, ctx: LootContext): boolean {
return composeConditions(entry.conditions ?? [])(ctx)
}
function createItem(entry: any, consumer: ItemConsumer, ctx: LootContext) {
const entryConsumer = decorateFunctions(entry.functions ?? [], consumer, ctx)
const type = entry.type?.replace(/^minecraft:/, '')
if (typeof entry.name !== 'string') {
return
}
switch (type) {
case 'item':
try {
entryConsumer(new ItemStack(Identifier.parse(entry.name), 1))
} catch (e) {}
break
case 'tag':
ctx.getItemTag(entry.name).forEach(tagEntry => {
try {
entryConsumer(new ItemStack(Identifier.parse(tagEntry), 1))
} catch (e) {}
})
break
case 'loot_table':
const lootTable = ctx.getLootTable(entry.name)
if (lootTable !== undefined) {
generateTable(lootTable, entryConsumer, ctx)
}
break
case 'dynamic':
// not relevant for this simulation
break
}
}
function computeWeight(entry: any, luck: number) {
return Math.max(Math.floor((entry.weight ?? 1) + (entry.quality ?? 0) * luck), 0)
}
type LootFunction = (item: ItemStack, ctx: LootContext) => void
function decorateFunctions(functions: any[], consumer: ItemConsumer, ctx: LootContext): ItemConsumer {
const compositeFunction = composeFunctions(functions)
return (item) => {
compositeFunction(item, ctx)
consumer(item)
}
}
function composeFunctions(functions: any[]): LootFunction {
return (item, ctx) => {
for (const fn of functions) {
if (Array.isArray(fn)) {
composeFunctions(fn)
} else if (isObject(fn) && composeConditions(fn.conditions ?? [])(ctx)) {
const type = fn.function?.replace(/^minecraft:/, '');
(LootFunctions[type]?.(fn) ?? (i => i))(item, ctx)
}
}
}
}
const LootFunctions: Record<string, (params: any) => LootFunction> = {
enchant_randomly: ({ enchantments }) => (item, ctx) => {
const isBook = item.is('book')
if (enchantments === undefined || enchantments.length === 0) {
enchantments = Enchantment.REGISTRY.map((_, ench) => ench)
.filter(ench => ench.isDiscoverable && (isBook || Enchantment.canEnchant(item, ench)))
.map(e => e.id.toString())
}
if (enchantments.length > 0) {
const id = enchantments[ctx.random.nextInt(enchantments.length)]
let ench: Enchantment | undefined
try {
ench = Enchantment.REGISTRY.get(Identifier.parse(id))
} catch (e) {}
if (ench === undefined) return
const lvl = ctx.random.nextInt(ench.maxLevel - ench.minLevel + 1) + ench.minLevel
if (isBook) {
item.tag = new NbtCompound()
item.count = 1
}
enchantItem(item, { id, lvl })
if (isBook) {
item.id = Identifier.create('enchanted_book')
}
}
},
enchant_with_levels: ({ levels, treasure }) => (item, ctx) => {
const enchants = selectEnchantments(ctx.random, item, computeInt(levels, ctx), treasure)
const isBook = item.is('book')
if (isBook) {
item.count = 1
item.tag = new NbtCompound()
}
for (const enchant of enchants) {
enchantItem(item, enchant)
}
if (isBook) {
item.id = Identifier.create('enchanted_book')
}
},
exploration_map: ({ decoration }) => (item) => {
if (!item.is('map')) {
return
}
item.id = Identifier.create('filled_map')
const color = decoration === 'mansion' ? 5393476 : decoration === 'monument' ? 3830373 : -1
if (color >= 0) {
getOrCreateTag(item, 'display').set('MapColor', new NbtInt(color))
}
},
limit_count: ({ limit }) => (item, ctx) => {
const { min, max } = prepareIntRange(limit, ctx)
item.count = clamp(item.count, min, max )
},
sequence: ({ functions }) => (item, ctx) => {
if (!Array.isArray(functions)) return
composeFunctions(functions)(item, ctx)
},
set_count: ({ count, add }) => (item, ctx) => {
const oldCount = add ? (item.count) : 0
item.count = clamp(oldCount + computeInt(count, ctx), 0, 64)
},
set_damage: ({ damage, add }) => (item, ctx) => {
const maxDamage = item.getItem().durability
if (maxDamage) {
const oldDamage = add ? 1 - item.tag.getNumber('Damage') / maxDamage : 0
const newDamage = 1 - clamp(computeFloat(damage, ctx) + oldDamage, 0, 1)
const finalDamage = Math.floor(newDamage * maxDamage)
item.tag.set('Damage', new NbtInt(finalDamage))
}
},
set_enchantments: ({ enchantments, add }) => (item, ctx) => {
if (!isObject(enchantments)) {
return
}
Object.entries(enchantments).forEach(([id, level]) => {
const lvl = computeInt(level, ctx)
try {
enchantItem(item, { id: Identifier.parse(id), lvl }, add)
} catch (e) {}
})
},
set_lore: ({ lore, replace }) => (item) => {
if (!Array.isArray(lore)) return
const lines: string[] = lore.flatMap((line: any) => line !== undefined ? [JSON.stringify(line)] : [])
const newLore = replace ? lines : [...item.tag.getCompound('display').getList('Lore', NbtType.String).map(s => s.getAsString()), ...lines]
getOrCreateTag(item, 'display').set('Lore', new NbtList(newLore.map(l => new NbtString(l))))
},
set_name: ({ name }) => (item) => {
if (name !== undefined) {
const newName = JSON.stringify(name)
getOrCreateTag(item, 'display').set('Name', new NbtString(newName))
}
},
set_nbt: ({ tag }) => (item) => {
try {
const newTag = NbtTag.fromString(tag)
if (newTag.isCompound()) {
item.tag = newTag
}
} catch (e) {}
},
set_potion: ({ id }) => (item) => {
if (typeof id === 'string') {
try {
item.tag.set('Potion', new NbtString(Identifier.parse(id).toString()))
} catch (e) {}
}
},
}
type LootCondition = (ctx: LootContext) => boolean
function composeConditions(conditions: any[]): LootCondition {
return (ctx) => {
for (const cond of conditions) {
if (!testCondition(cond, ctx)) {
return false
}
}
return true
}
}
function testCondition(condition: any, ctx: LootContext): boolean {
if (Array.isArray(condition)) {
return composeConditions(condition)(ctx)
}
if (!isObject(condition) || typeof condition.condition !== 'string') {
return false
}
const type = condition.condition?.replace(/^minecraft:/, '')
return (LootConditions[type]?.(condition) ?? (() => true))(ctx)
}
const LootConditions: Record<string, (params: any) => LootCondition> = {
alternative: params => LootConditions['any_of'](params),
all_of: ({ terms }) => (ctx) => {
if (!Array.isArray(terms) || terms.length === 0) return true
for (const term of terms) {
if (!testCondition(term, ctx)) {
return false
}
}
return true
},
any_of: ({ terms }) => (ctx) => {
if (!Array.isArray(terms) || terms.length === 0) return true
for (const term of terms) {
if (testCondition(term, ctx)) {
return true
}
}
return false
},
block_state_property: () => () => {
return false // TODO
},
damage_source_properties: ({ predicate }) => (ctx) => {
return testDamageSourcePredicate(predicate, ctx)
},
entity_properties: ({ predicate }) => (ctx) => {
return testEntityPredicate(predicate, ctx)
},
entity_scores: () => () => {
return false // TODO,
},
inverted: ({ term }) => (ctx) => {
return !testCondition(term, ctx)
},
killed_by_player: ({ inverted }) => () => {
return (inverted ?? false) === false // TODO
},
location_check: ({ predicate }) => (ctx) => {
return testLocationPredicate(predicate, ctx)
},
match_tool: ({ predicate }) => (ctx) => {
return testItemPredicate(predicate, ctx)
},
random_chance: ({ chance }) => (ctx) => {
return ctx.random.nextFloat() < chance
},
random_chance_with_looting: ({ chance, looting_multiplier }) => (ctx) => {
const level = 0 // TODO: get looting level from killer
const probability = chance + level * looting_multiplier
return ctx.random.nextFloat() < probability
},
reference: ({ name }) => (ctx) => {
const predicate = ctx.getPredicate(name) ?? []
if (Array.isArray(predicate)) {
return composeConditions(predicate)(ctx)
}
return testCondition(predicate, ctx)
},
survives_explosion: () => () => true,
table_bonus: ({ chances }) => (ctx) => {
if (!chances) {
return false
}
const level = 0 // TODO: get enchantment level from tool
const chance = chances[clamp(level, 0, chances.length - 1)]
return ctx.random.nextFloat() < chance
},
time_check: ({ value, period }) => (ctx) => {
let time = ctx.dayTime
if (period !== undefined) {
time = time % period
}
const { min, max } = prepareIntRange(value, ctx)
return min <= time && time <= max
},
value_check: () => () => {
return false // TODO
},
weather_check: ({ raining, thundering }) => (ctx) => {
const isRaining = ctx.weather === 'rain' || ctx.weather === 'thunder'
const isThundering = ctx.weather === 'thunder'
if (raining !== undefined && raining !== isRaining) return false
if (thundering !== undefined && thundering !== isThundering) return false
return true
},
}
function computeInt(provider: any, ctx: LootContext): number {
if (typeof provider === 'number') return Math.round(provider)
if (!isObject(provider)) return 0
const type = provider.type?.replace(/^minecraft:/, '') ?? 'uniform'
switch (type) {
case 'constant':
return Math.round(provider.value ?? 0)
case 'uniform':
const min = computeInt(provider.min, ctx)
const max = computeInt(provider.max, ctx)
return max < min ? min : ctx.random.nextInt(max - min + 1) + min
case 'binomial':
const n = computeInt(provider.n, ctx)
const p = computeFloat(provider.p, ctx)
let result = 0
for (let i = 0; i < n; i += 1) {
if (ctx.random.nextFloat() < p) {
result += 1
}
}
return result
}
return 0
}
function computeFloat(provider: any, ctx: LootContext): number {
if (typeof provider === 'number') return provider
if (!isObject(provider)) return 0
const type = provider.type?.replace(/^minecraft:/, '') ?? 'uniform'
switch (type) {
case 'constant':
return provider.value ?? 0
case 'uniform':
const min = computeFloat(provider.min, ctx)
const max = computeFloat(provider.max, ctx)
return max < min ? min : ctx.random.nextFloat() * (max-min) + min
case 'binomial':
const n = computeInt(provider.n, ctx)
const p = computeFloat(provider.p, ctx)
let result = 0
for (let i = 0; i < n; i += 1) {
if (ctx.random.nextFloat() < p) {
result += 1
}
}
return result
}
return 0
}
function prepareIntRange(range: any, ctx: LootContext) {
if (typeof range === 'number') {
range = { min: range, max: range }
}
const min = computeInt(range?.min, ctx)
const max = computeInt(range?.max, ctx)
return { min, max }
}
function testItemPredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function testLocationPredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function testEntityPredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function testDamageSourcePredicate(_predicate: any, _ctx: LootContext) {
return false // TODO
}
function enchantItem(item: ItemStack, enchant: Enchant, additive?: boolean) {
const listKey = item.is('book') ? 'StoredEnchantments' : 'Enchantments'
if (!item.tag.hasList(listKey, NbtType.Compound)) {
item.tag.set(listKey, new NbtList())
}
const enchantments = item.tag.getList(listKey, NbtType.Compound).getItems()
let index = enchantments.findIndex((e: any) => e.id === enchant.id)
if (index !== -1) {
const oldEnch = enchantments[index]
oldEnch.set('lvl', new NbtShort(Math.max(additive ? oldEnch.getNumber('lvl') + enchant.lvl : enchant.lvl, 0)))
} else {
enchantments.push(new NbtCompound().set('id', new NbtString(enchant.id.toString())).set('lvl', new NbtShort(enchant.lvl)))
index = enchantments.length - 1
}
if (enchantments[index].getNumber('lvl') === 0) {
enchantments.splice(index, 1)
}
item.tag.set(listKey, new NbtList(enchantments))
}
function selectEnchantments(random: Random, item: ItemStack, levels: number, treasure: boolean): Enchant[] {
const enchantmentValue = item.getItem().enchantmentValue
if (enchantmentValue === undefined) {
return []
}
levels += 1 + random.nextInt(Math.floor(enchantmentValue / 4 + 1)) + random.nextInt(Math.floor(enchantmentValue / 4 + 1))
const f = (random.nextFloat() + random.nextFloat() - 1) * 0.15
levels = clamp(Math.round(levels + levels * f), 1, Number.MAX_SAFE_INTEGER)
let available = getAvailableEnchantments(item, levels, treasure)
if (available.length === 0) {
return []
}
const result: Enchant[] = []
const first = getWeightedRandom(random, available, getEnchantWeight)
if (first) result.push(first)
while (random.nextInt(50) <= levels) {
if (result.length > 0) {
const lastAdded = result[result.length - 1]
available = available.filter(a => Enchantment.isCompatible(Enchantment.REGISTRY.getOrThrow(a.id), Enchantment.REGISTRY.getOrThrow(lastAdded.id)))
}
if (available.length === 0) break
const ench = getWeightedRandom(random, available, getEnchantWeight)
if (ench) result.push(ench)
levels = Math.floor(levels / 2)
}
return result
}
const EnchantmentsRarityWeights = new Map(Object.entries<number>({
common: 10,
uncommon: 5,
rare: 2,
very_rare: 1,
}))
function getEnchantWeight(ench: Enchant) {
return EnchantmentsRarityWeights.get(Enchantment.REGISTRY.get(ench.id)?.rarity ?? 'common') ?? 10
}
function getAvailableEnchantments(item: ItemStack, levels: number, treasure: boolean): Enchant[] {
const result: Enchant[] = []
const isBook = item.is('book')
Enchantment.REGISTRY.forEach((id, ench) => {
if ((!ench.isTreasure || treasure) && ench.isDiscoverable && (Enchantment.canEnchant(item, ench) || isBook)) {
for (let lvl = ench.maxLevel; lvl > ench.minLevel - 1; lvl -= 1) {
if (levels >= ench.minCost(lvl) && levels <= ench.maxCost(lvl)) {
result.push({ id, lvl })
}
}
}
})
return result
}
interface Enchant {
id: Identifier,
lvl: number,
}
const AlwaysHasGlint = new Set([
'minecraft:debug_stick',
'minecraft:enchanted_golden_apple',
'minecraft:enchanted_book',
'minecraft:end_crystal',
'minecraft:experience_bottle',
'minecraft:written_book',
])
export function itemHasGlint(item: ItemStack) {
if (AlwaysHasGlint.has(item.id.toString())) {
return true
}
if (item.is('compass') && (item.tag.has('LodestoneDimension') || item.tag.has('LodestonePos'))) {
return true
}
if ((item.is('potion') || item.is('splash_potion') || item.is('lingering_potion')) && (item.tag.has('Potion') || item.tag.has('CustomPotionEffects'))) {
return true
}
if (item.tag.getList('Enchantments').length > 0 || item.tag.getList('StoredEnchantments').length > 0) {
return true
}
return false
}
function getOrCreateTag(item: ItemStack, key: string) {
if (item.tag.hasCompound(key)) {
return item.tag.getCompound(key)
} else {
const tag = new NbtCompound()
item.tag.set(key, tag)
return tag
}
}

View File

@@ -1,16 +1,20 @@
import { DataModel } from '@mcschema/core'
import { useEffect, useRef, useState } from 'preact/hooks'
import { useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import { clamp, randomSeed } from '../../Utils.js'
import { useAsync } from '../../hooks/useAsync.js'
import { checkVersion, fetchAllPresets, fetchItemComponents } from '../../services/index.js'
import { clamp, jsonToNbt, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu, NumberInput } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import { ItemDisplay1204 } from '../ItemDisplay1204.jsx'
import type { PreviewProps } from './index.js'
import type { SlottedItem } from './LootTable.js'
import { generateLootTable } from './LootTable.js'
import { generateLootTable as generateLootTable1204 } from './LootTable1204.js'
export const LootTablePreview = ({ data }: PreviewProps) => {
export const LootTablePreview = ({ docAndNode }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const use1204 = !checkVersion(version, '1.20.5')
const [seed, setSeed] = useState(randomSeed())
const [luck, setLuck] = useState(0)
const [daytime, setDaytime] = useState(0)
@@ -19,22 +23,52 @@ export const LootTablePreview = ({ data }: PreviewProps) => {
const [advancedTooltips, setAdvancedTooltips] = useState(true)
const overlay = useRef<HTMLDivElement>(null)
const [items, setItems] = useState<SlottedItem[]>([])
const { value: dependencies, loading } = useAsync(() => {
return Promise.all([
fetchAllPresets(version, 'tag/item'),
fetchAllPresets(version, 'loot_table'),
use1204 ? Promise.resolve(undefined) : fetchItemComponents(version),
checkVersion(version, '1.21') ? fetchAllPresets(version, 'enchantment') : Promise.resolve(undefined),
checkVersion(version, '1.21') ? fetchAllPresets(version, 'tag/enchantment') : Promise.resolve(undefined),
])
}, [version])
const table = DataModel.unwrapLists(data)
const state = JSON.stringify(table)
useEffect(() => {
const items = generateLootTable(table, { version, seed, luck, daytime, weather, stackMixer: mixItems ? 'container' : 'default' })
console.log('Generated loot', items)
setItems(items)
}, [version, seed, luck, daytime, weather, mixItems, state])
const text = docAndNode.doc.getText()
const items = useMemo(() => {
if (dependencies === undefined || loading) {
return []
}
const [itemTags, lootTables, itemComponents, enchantments, enchantmentTags] = dependencies
const table = safeJsonParse(text) ?? {}
if (use1204) {
return generateLootTable1204(table, {
version, seed, luck, daytime, weather,
stackMixer: mixItems ? 'container' : 'default',
getItemTag: (id) => (itemTags.get(id.replace(/^minecraft:/, '')) as any)?.values ?? [],
getLootTable: (id) => lootTables.get(id.replace(/^minecraft:/, '')),
getPredicate: () => undefined,
})
}
return generateLootTable(table, {
version, seed, luck, daytime, weather,
stackMixer: mixItems ? 'container' : 'default',
getItemTag: (id) => (itemTags.get(id.replace(/^minecraft:/, '')) as any)?.values ?? [],
getLootTable: (id) => lootTables.get(id.replace(/^minecraft:/, '')),
getPredicate: () => undefined,
getEnchantments: () => enchantments ?? new Map(),
getEnchantmentTag: (id) => (enchantmentTags?.get(id.replace(/^minecraft:/, '')) as any)?.values ?? [],
getItemComponents: (id) => new Map([...(itemComponents?.get(id.toString()) ?? new Map()).entries()].map(([k, v]) => [k, jsonToNbt(v)])),
})
}, [version, seed, luck, daytime, weather, mixItems, text, dependencies, loading])
return <>
<div ref={overlay} class="preview-overlay">
<img src="/images/container.png" alt="Container background" class="pixelated" draggable={false} />
{items.map(({ slot, item }) =>
<div key={slot} style={slotStyle(slot)}>
<ItemDisplay item={item} slotDecoration={true} advancedTooltip={advancedTooltips} />
{use1204 ?
<ItemDisplay1204 item={item as any} slotDecoration={true} advancedTooltip={advancedTooltips} /> :
<ItemDisplay item={item as any} slotDecoration={true} advancedTooltip={advancedTooltips} />}
</div>
)}
</div>

View File

@@ -1,4 +1,3 @@
import { DataModel } from '@mcschema/core'
import { BlockDefinition, BlockModel, Identifier, Structure, StructureRenderer } from 'deepslate/render'
import type { mat4 } from 'gl-matrix'
import { useCallback, useRef } from 'preact/hooks'
@@ -6,33 +5,35 @@ import { useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { AsyncCancel } from '../../hooks/useAsyncFn.js'
import { getResources, ResourceWrapper } from '../../services/Resources.js'
import { safeJsonParse } from '../../Utils.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
const PREVIEW_ID = Identifier.parse('misode:preview')
const PREVIEW_DEFINITION = new BlockDefinition(PREVIEW_ID, { '': { model: PREVIEW_ID.toString() }}, undefined)
const PREVIEW_DEFINITION = new BlockDefinition({ '': { model: PREVIEW_ID.toString() }}, undefined)
export const ModelPreview = ({ data, shown }: PreviewProps) => {
export const ModelPreview = ({ docAndNode, shown }: PreviewProps) => {
const { version } = useVersion()
const serializedData = JSON.stringify(data)
const text = docAndNode.doc.getText()
const { value: resources } = useAsync(async () => {
if (!shown) return AsyncCancel
const resources = await getResources(version)
const model = BlockModel.fromJson(PREVIEW_ID.toString(), DataModel.unwrapLists(data))
model.flatten(resources)
const resources = await getResources(version, new Map())
const blockModel = BlockModel.fromJson(safeJsonParse(text) ?? {})
blockModel.flatten(resources)
const wrapper = new ResourceWrapper(resources, {
getBlockDefinition(id) {
if (id.equals(PREVIEW_ID)) return PREVIEW_DEFINITION
return null
},
getBlockModel(id) {
if (id.equals(PREVIEW_ID)) return model
if (id.equals(PREVIEW_ID)) return blockModel
return null
},
})
return wrapper
}, [shown, version, serializedData])
}, [shown, version, text])
const renderer = useRef<StructureRenderer | undefined>(undefined)

View File

@@ -1,10 +1,9 @@
import { DataModel } from '@mcschema/core'
import { clampedMap, NoiseParameters, NormalNoise, XoroshiroRandom } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
@@ -12,16 +11,17 @@ import { ColormapSelector } from './ColormapSelector.jsx'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const NoisePreview = ({ data, shown }: PreviewProps) => {
export const NoisePreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const [seed, setSeed] = useState(randomSeed())
const state = JSON.stringify(data)
const text = docAndNode.doc.getText()
const noise = useMemo(() => {
const random = XoroshiroRandom.create(seed)
const params = NoiseParameters.fromJson(DataModel.unwrapLists(data))
const params = NoiseParameters.fromJson(safeJsonParse(text) ?? {})
return new NormalNoise(random, params)
}, [state, seed])
}, [text, seed])
const imageData = useRef<ImageData>()
const ctx = useRef<CanvasRenderingContext2D>()

View File

@@ -1,38 +1,40 @@
import { DataModel } from '@mcschema/core'
import { clampedMap } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { vec2 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed } from '../../Utils.js'
import { getProjectData, useLocale, useProject } from '../../contexts/index.js'
import { useCallback, useRef, useState } from 'preact/hooks'
import { getWorldgenProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/index.js'
import { CachedCollections } from '../../services/index.js'
import { fetchRegistries } from '../../services/index.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnInput, BtnMenu, ErrorPanel } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { ColormapSelector } from './ColormapSelector.jsx'
import { DEEPSLATE } from './Deepslate.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) => {
export const NoiseSettingsPreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const { project } = useProject()
const [seed, setSeed] = useState(randomSeed())
const [biome, setBiome] = useState('minecraft:plains')
const [layer, setLayer] = useState('terrain')
const state = JSON.stringify(data)
const text = docAndNode.doc.getText()
const { value, error } = useAsync(async () => {
const unwrapped = DataModel.unwrapLists(data)
await DEEPSLATE.loadVersion(version, getProjectData(project))
const data = safeJsonParse(text) ?? {}
const projectData = await getWorldgenProjectData(project)
await DEEPSLATE.loadVersion(version, projectData)
const biomeSource = { type: 'fixed', biome }
await DEEPSLATE.loadChunkGenerator(unwrapped, biomeSource, seed)
await DEEPSLATE.loadChunkGenerator(data, biomeSource, seed)
const noiseSettings = DEEPSLATE.getNoiseSettings()
const finalDensity = DEEPSLATE.loadDensityFunction(unwrapped?.noise_router?.final_density, noiseSettings.minY, noiseSettings.height, seed)
const finalDensity = DEEPSLATE.loadDensityFunction(data?.noise_router?.final_density, noiseSettings.minY, noiseSettings.height, seed)
return { noiseSettings, finalDensity }
}, [state, seed, version, project, biome])
}, [text, seed, version, project, biome])
const { noiseSettings, finalDensity } = value ?? {}
const imageData = useRef<ImageData>()
@@ -86,7 +88,10 @@ export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) =>
}
}, [noiseSettings, finalDensity])
const allBiomes = useMemo(() => CachedCollections?.get('worldgen/biome') ?? [], [version])
const { value: allBiomes } = useAsync(async () => {
const registries = await fetchRegistries(version)
return registries.get('worldgen/biome')
}, [version])
if (error) {
return <ErrorPanel error={error} prefix="Failed to initialize preview: " />

View File

@@ -0,0 +1,173 @@
import { Identifier, ItemStack } from 'deepslate/core'
import type { VersionId } from '../../services/Versions.js'
import { checkVersion } from '../../services/Versions.js'
import { jsonToNbt } from '../../Utils.js'
export function placeItems(version: VersionId, recipe: any, animation: number, itemTags: Map<string, any>): Map<string, ItemStack> {
const items = new Map<string, ItemStack>()
const type: string = recipe.type?.replace(/^minecraft:/, '')
if (!type || type.startsWith('crafting_special') || type === 'crafting_decorated_pot') {
return items
}
if (type === 'crafting_shapeless') {
const ingredients: any[] = Array.isArray(recipe.ingredients) ? recipe.ingredients : []
ingredients.forEach((ingredient, i) => {
const choices = allIngredientChoices(version, ingredient, itemTags)
if (i >= 0 && i < 9 && choices.length > 0) {
const choice = choices[(3 * i + animation) % choices.length]
items.set(`crafting.${i}`, choice)
}
})
} else if (type === 'crafting_shaped') {
const keys = new Map<string, ItemStack>()
for (const [key, ingredient] of Object.entries(recipe.key ?? {})) {
const choices = allIngredientChoices(version, ingredient, itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
keys.set(key, choice)
}
}
const pattern = Array.isArray(recipe.pattern) ? recipe.pattern : []
for (let row = 0; row < Math.min(3, pattern.length); row += 1) {
for (let col = 0; col < Math.min(3, pattern[row].length); col += 1) {
const key = pattern[row].split('')[col]
const choice = key === ' ' ? undefined : keys.get(key)
if (choice) {
items.set(`crafting.${row * 3 + col}`, choice)
}
}
}
} else if (type === 'crafting_transmute') {
const inputs = allIngredientChoices(version, recipe.input, itemTags)
if (inputs.length > 0) {
const choice = inputs[animation % inputs.length]
items.set('crafting.0', choice)
}
const materials = allIngredientChoices(version, recipe.material, itemTags)
if (materials.length > 0) {
const choice = materials[animation % materials.length]
items.set('crafting.1', choice)
}
} else if (type === 'crafting_dye') {
const target = allIngredientChoices(version, recipe.target, itemTags)
if (target.length > 0) {
const choice = target[animation % target.length]
items.set('crafting.0', choice)
}
const dye = allIngredientChoices(version, recipe.dye, itemTags)
if (dye.length > 0) {
const choice = dye[animation % dye.length]
items.set('crafting.1', choice)
}
} else if (type === 'crafting_imbue') {
const source = allIngredientChoices(version, recipe.source, itemTags)
if (source.length > 0) {
const choice = source[animation % source.length]
items.set('crafting.4', choice)
}
const material = allIngredientChoices(version, recipe.material, itemTags)
if (material.length > 0) {
const choice = material[animation % material.length]
items.set('crafting.0', choice)
items.set('crafting.1', choice)
items.set('crafting.2', choice)
items.set('crafting.3', choice)
items.set('crafting.5', choice)
items.set('crafting.6', choice)
items.set('crafting.7', choice)
items.set('crafting.8', choice)
}
} else if (type === 'smelting' || type === 'smoking' || type === 'blasting' || type === 'campfire_cooking') {
const choices = allIngredientChoices(version, recipe.ingredient, itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
items.set('smelting.ingredient', choice)
}
} else if (type === 'stonecutting') {
const choices = allIngredientChoices(version, recipe.ingredient, itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
items.set('stonecutting.ingredient', choice)
}
} else if (type === 'smithing_transform' || type === 'smithing_trim') {
for (const ingredient of ['template', 'base', 'addition'] as const) {
const choices = allIngredientChoices(version, recipe[ingredient], itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
items.set(`smithing.${ingredient}`, choice)
}
}
}
let resultSlot = 'crafting.result'
if (type === 'smelting' || type === 'smoking' || type === 'blasting' || type === 'campfire_cooking') {
resultSlot = 'smelting.result'
} else if (type === 'stonecutting') {
resultSlot = 'stonecutting.result'
} else if (type === 'smithing_transform' || type === 'smithing_trim') {
resultSlot = 'smithing.result'
}
const result = recipe.result
if (type === 'smithing_trim') {
const base = items.get('smithing.base')
if (base) {
items.set(resultSlot, base)
}
} else if (typeof result === 'string') {
items.set(resultSlot, new ItemStack(Identifier.parse(result), 1))
} else if (typeof result === 'object' && result !== null) {
const id = typeof result.id === 'string' ? result.id
: typeof result.item === 'string' ? result.item
: 'minecraft:air'
if (id !== 'minecraft:air') {
const count = typeof result.count === 'number' ? result.count : 1
const components = new Map(Object.entries(result.components ?? {})
.map(([k, v]) => [k, jsonToNbt(v)]))
items.set(resultSlot, new ItemStack(Identifier.parse(id), count, components))
}
}
return items
}
function allIngredientChoices(version: VersionId, ingredient: any, itemTags: Map<string, any>): ItemStack[] {
if (Array.isArray(ingredient)) {
return ingredient.flatMap(i => allIngredientChoices(version, i, itemTags))
}
if (checkVersion(version, '1.21.2')) {
if (ingredient !== null) {
if (typeof ingredient === 'string') {
if (ingredient.startsWith('#')) {
return parseTag(version, ingredient.slice(1), itemTags)
}
return [new ItemStack(Identifier.parse(ingredient), 1)]
}
}
return [new ItemStack(Identifier.create('stone'), 1)]
} else {
if (typeof ingredient === 'object' && ingredient !== null) {
if (typeof ingredient.item === 'string') {
return [new ItemStack(Identifier.parse(ingredient.item), 1)]
} else if (typeof ingredient.tag === 'string') {
return parseTag(version, ingredient.tag, itemTags)
}
}
}
return []
}
function parseTag(version: VersionId, tagId: any, itemTags: Map<string, any>): ItemStack[] {
const tag: any = itemTags.get(tagId.replace(/^minecraft:/, ''))
if (typeof tag === 'object' && tag !== null && Array.isArray(tag.values)) {
return tag.values.flatMap((value: any) => {
if (typeof value !== 'string') return []
if (value.startsWith('#')) return parseTag(version, value.slice(1), itemTags)
return [new ItemStack(Identifier.parse(value), 1)]
})
}
return []
}

View File

@@ -0,0 +1,127 @@
import { Identifier, ItemStack } from 'deepslate-1.20.4/core'
import type { VersionId } from '../../services/Versions.js'
export function placeItems(version: VersionId, recipe: any, animation: number, itemTags: Map<string, any>): Map<string, ItemStack> {
const items = new Map<string, ItemStack>()
const type: string = recipe.type?.replace(/^minecraft:/, '')
if (!type || type.startsWith('crafting_special') || type === 'crafting_decorated_pot') {
return items
}
if (type === 'crafting_shapeless') {
const ingredients: any[] = Array.isArray(recipe.ingredients) ? recipe.ingredients : []
ingredients.forEach((ingredient, i) => {
const choices = allIngredientChoices(version, ingredient, itemTags)
if (i >= 0 && i < 9 && choices.length > 0) {
const choice = choices[(3 * i + animation) % choices.length]
items.set(`crafting.${i}`, choice)
}
})
} else if (type === 'crafting_shaped') {
const keys = new Map<string, ItemStack>()
for (const [key, ingredient] of Object.entries(recipe.key ?? {})) {
const choices = allIngredientChoices(version, ingredient, itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
keys.set(key, choice)
}
}
const pattern = Array.isArray(recipe.pattern) ? recipe.pattern : []
for (let row = 0; row < Math.min(3, pattern.length); row += 1) {
for (let col = 0; col < Math.min(3, pattern[row].length); col += 1) {
const key = pattern[row].split('')[col]
const choice = key === ' ' ? undefined : keys.get(key)
if (choice) {
items.set(`crafting.${row * 3 + col}`, choice)
}
}
}
} else if (type === 'crafting_transmute') {
const inputs = allIngredientChoices(version, recipe.input, itemTags)
if (inputs.length > 0) {
const choice = inputs[animation % inputs.length]
items.set('crafting.0', choice)
}
const materials = allIngredientChoices(version, recipe.material, itemTags)
if (materials.length > 0) {
const choice = materials[animation % materials.length]
items.set('crafting.1', choice)
}
} else if (type === 'smelting' || type === 'smoking' || type === 'blasting' || type === 'campfire_cooking') {
const choices = allIngredientChoices(version, recipe.ingredient, itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
items.set('smelting.ingredient', choice)
}
} else if (type === 'stonecutting') {
const choices = allIngredientChoices(version, recipe.ingredient, itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
items.set('stonecutting.ingredient', choice)
}
} else if (type === 'smithing_transform' || type === 'smithing_trim') {
for (const ingredient of ['template', 'base', 'addition'] as const) {
const choices = allIngredientChoices(version, recipe[ingredient], itemTags)
if (choices.length > 0) {
const choice = choices[animation % choices.length]
items.set(`smithing.${ingredient}`, choice)
}
}
}
let resultSlot = 'crafting.result'
if (type === 'smelting' || type === 'smoking' || type === 'blasting' || type === 'campfire_cooking') {
resultSlot = 'smelting.result'
} else if (type === 'stonecutting') {
resultSlot = 'stonecutting.result'
} else if (type === 'smithing_transform' || type === 'smithing_trim') {
resultSlot = 'smithing.result'
}
const result = recipe.result
if (type === 'smithing_trim') {
const base = items.get('smithing.base')
if (base) {
items.set(resultSlot, base)
}
} else if (typeof result === 'string') {
items.set(resultSlot, new ItemStack(Identifier.parse(result), 1))
} else if (typeof result === 'object' && result !== null) {
const id = typeof result.id === 'string' ? result.id
: typeof result.item === 'string' ? result.item
: 'minecraft:air'
if (id !== 'minecraft:air') {
const count = typeof result.count === 'number' ? result.count : 1
items.set(resultSlot, new ItemStack(Identifier.parse(id), count))
}
}
return items
}
function allIngredientChoices(version: VersionId, ingredient: any, itemTags: Map<string, any>): ItemStack[] {
if (Array.isArray(ingredient)) {
return ingredient.flatMap(i => allIngredientChoices(version, i, itemTags))
}
if (typeof ingredient === 'object' && ingredient !== null) {
if (typeof ingredient.item === 'string') {
return [new ItemStack(Identifier.parse(ingredient.item), 1)]
} else if (typeof ingredient.tag === 'string') {
return parseTag(version, ingredient.tag, itemTags)
}
}
return []
}
function parseTag(version: VersionId, tagId: any, itemTags: Map<string, any>): ItemStack[] {
const tag: any = itemTags.get(tagId.replace(/^minecraft:/, ''))
if (typeof tag === 'object' && tag !== null && Array.isArray(tag.values)) {
return tag.values.flatMap((value: any) => {
if (typeof value !== 'string') return []
if (value.startsWith('#')) return parseTag(version, value.slice(1), itemTags)
return [new ItemStack(Identifier.parse(value), 1)]
})
}
return []
}

View File

@@ -0,0 +1,108 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { checkVersion, fetchAllPresets } from '../../services/index.js'
import { safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import { ItemDisplay1204 } from '../ItemDisplay1204.jsx'
import type { PreviewProps } from './index.js'
import { placeItems } from './Recipe.js'
import { placeItems as placeItems1204 } from './Recipe1204.js'
const ANIMATION_TIME = 1000
export const RecipePreview = ({ docAndNode }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const use1204 = !checkVersion(version, '1.20.5')
const [advancedTooltips, setAdvancedTooltips] = useState(true)
const [animation, setAnimation] = useState(0)
const overlay = useRef<HTMLDivElement>(null)
const { value: itemTags } = useAsync(() => {
return fetchAllPresets(version, 'tag/item')
}, [version])
useEffect(() => {
const interval = setInterval(() => {
setAnimation(n => n + 1)
}, ANIMATION_TIME)
return () => clearInterval(interval)
}, [])
const text = docAndNode.doc.getText()
const recipe = safeJsonParse(text) ?? {}
const items = useMemo(() => {
if (use1204) {
return placeItems1204(version, recipe, animation, itemTags ?? new Map())
}
return placeItems(version, recipe, animation, itemTags ?? new Map())
}, [use1204, text, animation, itemTags])
const gui = useMemo(() => {
const type = recipe?.type?.replace(/^minecraft:/, '')
if (type === 'smelting' || type === 'blasting' || type === 'smoking' || type === 'campfire_cooking') {
return '/images/furnace.png'
} else if (type === 'stonecutting') {
return '/images/stonecutter.png'
} else if (type === 'smithing_transform' || type === 'smithing_trim') {
return '/images/smithing.png'
} else {
return '/images/crafting_table.png'
}
}, [text])
return <>
<div ref={overlay} class="preview-overlay">
<img src={gui} alt="Crafting GUI" class="pixelated" draggable={false} />
{[...items.entries()].map(([slot, item]) =>
<div key={slot} style={slotStyle(slot)}>
{use1204
? <ItemDisplay1204 item={item as any} slotDecoration={true} advancedTooltip={advancedTooltips} />
: <ItemDisplay item={item as any} slotDecoration={true} advancedTooltip={advancedTooltips} />}
</div>
)}
</div>
<div class="controls preview-controls">
<BtnMenu icon="gear" tooltip={locale('settings')} >
<Btn icon={advancedTooltips ? 'square_fill' : 'square'} label="Advanced tooltips" onClick={e => {setAdvancedTooltips(!advancedTooltips); e.stopPropagation()}} />
</BtnMenu>
</div>
</>
}
const GUI_WIDTH = 176
const GUI_HEIGHT = 81
const SLOT_SIZE = 18
const SLOTS: Record<string, [number, number]> = {
'crafting.0': [29, 16],
'crafting.1': [47, 16],
'crafting.2': [65, 16],
'crafting.3': [29, 34],
'crafting.4': [47, 34],
'crafting.5': [65, 34],
'crafting.6': [29, 52],
'crafting.7': [47, 52],
'crafting.8': [65, 52],
'crafting.result': [123, 34],
'smelting.ingredient': [55, 16],
'smelting.fuel': [55, 53],
'smelting.result': [115, 34],
'stonecutting.ingredient': [19, 32],
'stonecutting.result': [142, 32],
'smithing.template': [7, 47],
'smithing.base': [25, 47],
'smithing.addition': [43, 47],
'smithing.result': [97, 47],
}
function slotStyle(slot: string) {
const [x, y] = SLOTS[slot] ?? [0, 0]
return {
left: `${x*100/GUI_WIDTH}%`,
top: `${y*100/GUI_HEIGHT}%`,
width: `${SLOT_SIZE*100/GUI_WIDTH}%`,
height: `${SLOT_SIZE*100/GUI_HEIGHT}%`,
}
}

View File

@@ -1,28 +1,29 @@
import { DataModel } from '@mcschema/core'
import type { Identifier } from 'deepslate'
import { ChunkPos } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
import type { Color } from '../../Utils.js'
import { computeIfAbsent, iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js'
import { useLocale } from '../../contexts/index.js'
import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import type { Color } from '../../Utils.js'
import { computeIfAbsent, iterateWorld2D, randomSeed, safeJsonParse, stringToColor } from '../../Utils.js'
import { Btn } from '../index.js'
import { featureColors } from './Decorator.js'
import { DEEPSLATE } from './Deepslate.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const StructureSetPreview = ({ data, version, shown }: PreviewProps) => {
export const StructureSetPreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const [seed, setSeed] = useState(randomSeed())
const state = JSON.stringify(data)
const text = docAndNode.doc.getText()
const { value: structureSet } = useAsync(async () => {
await DEEPSLATE.loadVersion(version)
const structureSet = DEEPSLATE.loadStructureSet(DataModel.unwrapLists(data), seed)
const structureSet = DEEPSLATE.loadStructureSet(safeJsonParse(text) ?? {}, seed)
return structureSet
}, [state, version, seed])
}, [text, version, seed])
const { chunkStructures, structureColors } = useMemo(() => {
return {
@@ -51,7 +52,7 @@ export const StructureSetPreview = ({ data, version, shown }: PreviewProps) => {
iterateWorld2D(imageData.current, transform, (x, y) => {
const pos = ChunkPos.create(x, y)
const structure = computeIfAbsent(chunkStructures, `${pos[0]} ${pos[1]}`, () => structureSet?.getStructureInChunk(pos[0], pos[1], context))
const structure = computeIfAbsent(chunkStructures, `${pos[0]} ${pos[1]}`, () => structureSet?.getStructureInChunk(pos[0], pos[1], context)?.id)
return { structure, pos }
}, ({ structure, pos }) => {
if (structure !== undefined) {

View File

@@ -1,19 +1,19 @@
import type { DataModel } from '@mcschema/core'
import type { VersionId } from '../../services/index.js'
import type { DocAndNode } from '@spyglassmc/core'
export * from './BiomeSourcePreview.js'
export * from './BlockStatePreview.jsx'
export * from './DecoratorPreview.js'
export * from './DensityFunctionPreview.js'
export * from './DialogPreview.js'
export * from './ItemModelPreview.jsx'
export * from './LootTablePreview.jsx'
export * from './ModelPreview.jsx'
export * from './NoisePreview.js'
export * from './NoiseSettingsPreview.js'
export * from './RecipePreview.jsx'
export * from './StructureSetPreview.jsx'
export type PreviewProps = {
model: DataModel,
data: any,
shown: boolean,
version: VersionId,
export interface PreviewProps {
docAndNode: DocAndNode
shown: boolean
}

View File

@@ -8,6 +8,7 @@ import { Btn, TextInput } from '../index.js'
import { ChangelogEntry } from './ChangelogEntry.js'
const SEARCH_KEY = 'search'
const REPO = 'https://github.com/misode/technical-changes'
interface Props {
changes: Change[] | undefined,
@@ -75,6 +76,9 @@ export function ChangelogList({ changes, defaultOrder, limit, navigation }: Prop
{hiddenChanges > 0 && (
<Btn label={locale('changelog.show_more', `${hiddenChanges}`)} onClick={() => setLimitActive(false)}/>
)}
<span class="note py-2">
<a href={REPO} target="_blank">{locale('changelog.edit_on_github')}</a>
</span>
</div>
</>
}

View File

@@ -6,7 +6,7 @@ interface Props {
fix: Bugfix
}
export function Issue({ fix }: Props) {
return <Card overlay={fix.id} link={`https://bugs.mojang.com/browse/${fix.id}`}>
return <Card overlay={fix.id} link={`https://mojira.dev/${fix.id}`}>
<div class="changelog-content">{fix.summary}</div>
<div class="badges-list">
{fix.categories.map(c => <Badge label={c} />)}

View File

@@ -9,6 +9,7 @@ import { Octicon } from '../Octicon.js'
import { ChangelogList, IssueList, VersionDiff, VersionMetaData } from './index.js'
const Tabs = ['changelog', 'diff', 'fixes']
const WIKI_PAGE_PREFIX = 'https://minecraft.wiki/w/Java_Edition_'
interface Props {
id: string,
@@ -31,6 +32,7 @@ export function VersionDetail({ id, version }: Props) {
[id, changes])
const articleLink = version && getArticleLink(version.id)
const wikiPageLink = version && WIKI_PAGE_PREFIX + version.name
return <>
<div class="version-detail">
@@ -41,8 +43,8 @@ export function VersionDetail({ id, version }: Props) {
{version.release_target !== null && <VersionMetaData label={locale('versions.release_target')} value={version.release_target} link={version.id !== version.release_target ? `/versions/?id=${version.release_target}` : undefined} />}
<VersionMetaData label={locale('versions.data_version')} value={version.data_version} />
<VersionMetaData label={locale('versions.protocol_version')} value={version.protocol_version} />
<VersionMetaData label={locale('versions.data_pack_format')} value={version.data_pack_version} />
<VersionMetaData label={locale('versions.resource_pack_format')} value={version.resource_pack_version} />
<VersionMetaData label={locale('versions.data_pack_format')} value={new Date(version.release_time) > new Date(2025, 6, 28) ? `${version.data_pack_version}.${version.data_pack_version_minor}` : version.data_pack_version} />
<VersionMetaData label={locale('versions.resource_pack_format')} value={new Date(version.release_time) > new Date(2025, 6, 28) ? `${version.resource_pack_version}.${version.resource_pack_version_minor}` : version.resource_pack_version} />
</> : filteredChangelogs?.length ?? 0 > 1 ? <p>
This version is not released yet.
</p> : <p>
@@ -57,6 +59,10 @@ export function VersionDetail({ id, version }: Props) {
{locale('versions.article')}
{Octicon.link_external}
</a>}
{wikiPageLink && <a href={wikiPageLink} target="_blank">
{locale('versions.wiki')}
{Octicon.link_external}
</a>}
</div>
<div class="version-tab">
{tab === 'changelog' && <ChangelogList changes={filteredChangelogs} defaultOrder="asc" />}

View File

@@ -1,13 +1,16 @@
import { useCallback, useEffect, useMemo, useRef } from "preact/hooks"
import { parseGitPatch } from "../../Utils.js"
import { useLocale } from "../../contexts/Locale.jsx"
import { useAsync } from "../../hooks/useAsync.js"
import { useLocalStorage } from "../../hooks/useLocalStorage.js"
import { useSearchParam } from "../../hooks/useSearchParam.js"
import { GitHubCommitFile, fetchVersionDiff } from "../../services/DataFetcher.js"
import { ErrorPanel } from "../ErrorPanel.jsx"
import { Octicon } from "../Octicon.jsx"
import { TreeView, TreeViewGroupRenderer, TreeViewLeafRenderer } from "../TreeView.jsx"
import { createPatch } from 'diff'
import { useCallback, useEffect, useRef } from 'preact/hooks'
import { useLocale } from '../../contexts/Locale.jsx'
import { useAsync } from '../../hooks/useAsync.js'
import { useLocalStorage } from '../../hooks/useLocalStorage.js'
import { useSearchParam } from '../../hooks/useSearchParam.js'
import type { GitHubCommitFile } from '../../services/DataFetcher.js'
import { fetchVersionDiff } from '../../services/DataFetcher.js'
import { parseGitPatch } from '../../Utils.js'
import { ErrorPanel } from '../ErrorPanel.jsx'
import { Octicon } from '../Octicon.jsx'
import type { TreeViewGroupRenderer, TreeViewLeafRenderer } from '../TreeView.jsx'
import { TreeView } from '../TreeView.jsx'
const mcmetaRawUrl = 'https://raw.githubusercontent.com/misode/mcmeta'
const mcmetaBlobUrl = 'https://github.com/misode/mcmeta/blob'
@@ -30,35 +33,45 @@ export function VersionDiff({ version }: Props) {
}
}, [diffView, setFilename])
const { file, diff } = useMemo(() => {
const { value, loading } = useAsync(async () => {
if (filename === undefined) return { file: undefined, diff: undefined }
const file = commit?.files.find(f => f.filename === filename)
if (file === undefined) return { file, diff: undefined }
if (file.patch === undefined) {
const match = filename.match(/\.(png|ogg)$/)
if (match) {
let patch = file.patch
if (patch === undefined) {
const isMedia = filename.match(/\.(png|ogg)$/)
const isText = filename.match(/\.(txt|json|mcmeta|snbt|vsh|fsh)$/)
if (isMedia) {
return {
file,
diff: {
type: match[1],
type: isMedia[1],
before: file.status === 'added' ? undefined : `${mcmetaRawUrl}/${commit?.parents[0].sha}/${filename}`,
after: file.status === 'removed' ? undefined : `${mcmetaRawUrl}/${version}-diff/${filename}`,
},
}
} else if (file.status === 'renamed') {
return { file, diff: [] }
} else {
return { file, diff: new Error('Cannot display diff for this file') }
} else if (isText) {
const [beforeStr, afterStr] = await Promise.all([
fetch(`${mcmetaRawUrl}/${commit?.parents[0].sha}/${filename}`).then(r => r.ok ? r.text() : ''),
fetch(`${mcmetaRawUrl}/${version}-diff/${filename}`).then(r => r.ok ? r.text() : ''),
])
patch = createPatch(filename, beforeStr, afterStr)
}
}
if (patch === undefined) {
return { file, diff: new Error('Cannot display diff for this file') }
}
try {
return { file, diff: parseGitPatch(file.patch) }
return { file, diff: parseGitPatch(patch) }
} catch (e) {
const error = e as Error
error.message = `Failed to show diff: ${error.message}`
return { file, diff: error }
}
}, [filename, commit])
const { file, diff } = value ?? {}
const DiffFolder: TreeViewGroupRenderer = useCallback(({ name, open, onClick }) => {
return <div class="diff-entry select-none" onClick={onClick} >
@@ -71,7 +84,7 @@ export function VersionDiff({ version }: Props) {
return <div class={`diff-entry py-0.5 flex items-center [&>svg]:shrink-0 select-none ${entry.filename === filename ? 'active' : ''}`} onClick={() => selectFile(entry.filename)} title={entry.filename}>
<span class="ml-[15px] mx-2 overflow-hidden text-ellipsis whitespace-nowrap">{entry.filename.split('/').at(-1)}</span>
<span class={`ml-auto diff-${entry.status}`}>{Octicon[`diff_${entry.status}`]}</span>
</div>
</div>
}, [filename])
useEffect(() => {
@@ -106,12 +119,12 @@ export function VersionDiff({ version }: Props) {
<div class={`diff-tree w-full md:w-64 md:overflow-y-scroll md:overscroll-contain md:sticky md:top-[56px] ${filename ? 'hidden md:block' : 'block'}`}>
<TreeView entries={commit?.files ?? []} group={DiffFolder} leaf={DiffEntry} split={file => file.filename.split('/')} />
</div>
{filename && <div key={filename} class={`diff-view-panel flex-1 min-w-0 md:pl-2 md:ml-64`}>
{filename && <div key={filename} class={'diff-view-panel flex-1 min-w-0 md:pl-2 md:ml-64'}>
<div class="flex justify-center items-center min-w-0 text-center py-2" title={filename}>
<span class="mr-2 min-w-0 overflow-hidden text-ellipsis font-bold text-xl">{filename}</span>
<a class="diff-toggle p-1" href={`${mcmetaBlobUrl}/${version}-diff/${filename}`} target="_blank">{Octicon.link_external}</a>
</div>
{diff === undefined ? (
{(diff === undefined || loading) ? (
<span class="note">{locale('loading')}</span>
) : diff instanceof Error ? (
<ErrorPanel error={diff} />
@@ -134,7 +147,7 @@ export function VersionDiff({ version }: Props) {
)}
</div>
) : <>
{file.previous_filename !== undefined && <div class="flex justify-center font-mono flex-wrap" title={`${file.previous_filename}${filename}`}>
{file?.previous_filename !== undefined && <div class="flex justify-center font-mono flex-wrap" title={`${file.previous_filename}${filename}`}>
<span class="overflow-hidden text-ellipsis mr-2">{file.previous_filename}</span>
<span class="overflow-hidden text-ellipsis whitespace-nowrap"><span class="select-none"> </span>{filename}</span>
</div>}

View File

@@ -13,6 +13,6 @@ export function VersionEntry({ version, link }: Props) {
<span class="version-id">{version.id}</span>
<VersionMetaData label={locale('versions.released')} value={releaseDate(version)} compact />
<VersionMetaData label={locale('versions.data_version')} value={version.data_version} optional />
<VersionMetaData label={locale('versions.pack_format')} value={version.data_pack_version} optional />
<VersionMetaData label={locale('versions.data_pack_format')} value={new Date(version.release_time) > new Date(2025, 6, 28) ? `${version.data_pack_version}.${version.data_pack_version_minor}` : version.data_pack_version} optional />
</a>
}

View File

@@ -46,13 +46,7 @@ async function loadLocale(language: string) {
const langConfig = config.languages.find(lang => lang.code === language)
if (!langConfig) return
const data = await import(`../../locales/${language}.json`)
const schema = langConfig.schemas !== false
&& await import(`../../../node_modules/@mcschema/locales/src/${language}.json`)
let partners = { default: {} }
if (language === 'en') {
partners = await import('../partners/locales/en.json')
}
Locales[language] = { ...data.default, ...schema.default, ...partners.default }
Locales[language] = data.default
}
export function useLocale() {

Some files were not shown because too many files have changed in this diff Show More