Skip to content

Commit a948715

Browse files
committed
Add /give to loot table converter tool
1 parent fb95b38 commit a948715

File tree

7 files changed

+359
-5
lines changed

7 files changed

+359
-5
lines changed

src/app/App.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { Router } from 'preact-router'
33
import '../styles/global.css'
44
import '../styles/nodes.css'
55
import { Analytics } from './Analytics.js'
6-
import { cleanUrl } from './Utils.js'
76
import { Header } from './components/index.js'
8-
import { Changelog, Customized, Generator, Generators, Guide, Guides, Home, LegacyPartners, Partners, Sounds, Transformation, Versions, WhatsNew, Worldgen } from './pages/index.js'
7+
import { Changelog, Convert, Customized, Generator, Generators, Guide, Guides, Home, LegacyPartners, Partners, Sounds, Transformation, Versions, WhatsNew, Worldgen } from './pages/index.js'
8+
import { cleanUrl } from './Utils.js'
99

1010
export function App() {
1111
const changeRoute = (e: RouterOnChangeArgs) => {
@@ -27,6 +27,8 @@ export function App() {
2727
<Versions path="/versions" />
2828
<Transformation path="/transformation" />
2929
<Customized path="/customized" />
30+
<Convert path="/convert" />
31+
<Convert path="/convert/:formats" />
3032
<WhatsNew path="/whats-new" />
3133
<Guides path="/guides" />
3234
<Guide path="/guides/:id" />

src/app/components/Icons.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export const Icons = {
66
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>,
77
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>,
88
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>,
9+
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"/>
10+
</svg>,
911
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>,
1012
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>,
1113
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>,

src/app/pages/Convert.tsx

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import { Identifier, ItemStack, Json, NbtCompound, NbtString, NbtTag, StringReader } from 'deepslate'
2+
import { route } from 'preact-router'
3+
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
4+
import config from '../../config.json'
5+
import { Footer, Octicon } from '../components/index.js'
6+
import { useLocale } from '../contexts/Locale.jsx'
7+
import { useTitle } from '../contexts/Title.jsx'
8+
import { useActiveTimeout } from '../hooks/useActiveTimout.js'
9+
import { useLocalStorage } from '../hooks/useLocalStorage.js'
10+
import type { VersionId } from '../services/Versions.js'
11+
import { checkVersion } from '../services/Versions.js'
12+
import { jsonToNbt } from '../Utils.js'
13+
14+
const FORMATS = ['give-command', 'loot-table'] as const
15+
type Format = typeof FORMATS[number]
16+
17+
interface Props {
18+
path?: string,
19+
formats?: string,
20+
}
21+
export function Convert({ formats }: Props) {
22+
const {locale} = useLocale()
23+
24+
const [source, setSource] = useState<Format>()
25+
const [target, setTarget] = useState<Format>()
26+
27+
useEffect(() => {
28+
const match = formats?.match(/^([a-z0-9-]+)-to-([a-z0-9-]+)/)
29+
if (match && FORMATS.includes(match[1] as Format)) {
30+
setSource(match[1] as Format)
31+
}
32+
if (match && FORMATS.includes(match[2] as Format)) {
33+
setTarget(match[2] as Format)
34+
}
35+
}, [formats])
36+
37+
const supportedVersions = useMemo(() => {
38+
return config.versions
39+
.filter(v => checkVersion(v.id, '1.20.5'))
40+
.map(v => v.id as VersionId)
41+
.reverse()
42+
}, [])
43+
44+
const title = !source || !target
45+
? locale('title.convert')
46+
: locale('title.convert.formats', locale(`convert.format.${source}`), locale(`convert.format.${target}`))
47+
useTitle(title, supportedVersions)
48+
49+
const [input, setInput] = useLocalStorage('misode_convert_input', '')
50+
51+
const convertFn = useMemo(() => {
52+
if (!source || !target) {
53+
return undefined
54+
}
55+
if (source === target) {
56+
return (input: string) => input
57+
}
58+
return CONVERSIONS[source][target]
59+
}, [source, target])
60+
61+
const { output, error } = useMemo(() => {
62+
if (!convertFn) {
63+
return { output: '' }
64+
}
65+
try {
66+
return { output: convertFn(input) }
67+
} catch (e) {
68+
return { output: '', error: e instanceof Error ? e : undefined }
69+
}
70+
}, [convertFn, input])
71+
72+
const changeSource = useCallback((newSource: Format) => {
73+
setSource(newSource)
74+
if (target === newSource) {
75+
setTarget(source)
76+
setInput(output)
77+
}
78+
if (target) {
79+
route(`/convert/${newSource}-to-${target === newSource ? source : target}`)
80+
}
81+
}, [source, target])
82+
83+
const changeTarget = useCallback((newTarget: Format) => {
84+
setTarget(newTarget)
85+
if (source === newTarget) {
86+
setSource(target)
87+
setInput(output)
88+
}
89+
if (source) {
90+
route(`/convert/${source === newTarget ? target : source}-to-${newTarget}`)
91+
}
92+
}, [source])
93+
94+
const onSwap = useCallback(() => {
95+
setSource(target)
96+
setTarget(source)
97+
if (output.length > 0) {
98+
setInput(output)
99+
}
100+
if (source && target) {
101+
route(`/convert/${target}-to-${source}`)
102+
}
103+
}, [source, target, output])
104+
105+
const [copyActive, setCopyActive] = useActiveTimeout()
106+
const onCopyOutput = useCallback(async () => {
107+
await navigator.clipboard.writeText(output)
108+
setCopyActive()
109+
}, [output])
110+
111+
return <main>
112+
<div class="legacy-container">
113+
<div class="flex my-4 justify-center">
114+
<FormatSelect value={source} onChange={changeSource} />
115+
<button class="mx-3 tooltipped tip-s" aria-label={locale('convert.swap')} onClick={onSwap}>{Octicon.arrow_switch}</button>
116+
<FormatSelect value={target} onChange={changeTarget} />
117+
</div>
118+
<div class="flex">
119+
<div class="relative w-full mr-2">
120+
<textarea class="convert-textarea block resize-none w-full font-mono text-sm px-2 py-1 rounded" value={input} onInput={(e) => setInput((e.target as HTMLTextAreaElement).value)}></textarea>
121+
{error && <div class="convert-error absolute bottom-0 left-0 w-full p-2 pr-6">{error.message}</div>}
122+
</div>
123+
<div class="relative w-full ml-2">
124+
<textarea class="convert-textarea block resize-none w-full font-mono text-sm px-2 py-1 rounded" value={output} readonly></textarea>
125+
<button class={`absolute top-0 right-0 m-4 mr-5 tooltipped tip-s ${copyActive ? 'status-icon active' : ''}`} aria-label={locale(copyActive ? 'copied' : 'copy')} onClick={onCopyOutput}>{copyActive ? Octicon.check : Octicon.copy}</button>
126+
</div>
127+
</div>
128+
</div>
129+
<Footer />
130+
</main>
131+
}
132+
133+
interface FormatSelectProps {
134+
value: string | undefined
135+
onChange: (newValue: Format) => void
136+
}
137+
function FormatSelect({ value, onChange }: FormatSelectProps) {
138+
const { locale } = useLocale()
139+
return <select class="convert-select text-xl px-3 py-1 rounded" value={value} onChange={(e) => onChange((e.target as HTMLSelectElement).value as Format)}>
140+
{value === undefined && <option value={undefined}>{locale('convert.select')}</option>}
141+
{FORMATS.map(format => <option value={format}>{locale(`convert.format.${format}`)}</option>)}
142+
</select>
143+
}
144+
145+
const CONVERSIONS: Record<Format, Partial<Record<Format, (input: string) => string>>> = {
146+
'give-command': {
147+
'loot-table': (input) => {
148+
const itemStack = parseGiveCommand(new StringReader(input))
149+
const lootTable = createLootTable(itemStack)
150+
return JSON.stringify(lootTable, null, 2)
151+
},
152+
},
153+
'loot-table': {
154+
'give-command': (input) => {
155+
const lootTable = JSON.parse(input)
156+
const itemStack = getItemFromLootTable(lootTable)
157+
return `give @s ${stringifyItemStack(itemStack)}`
158+
},
159+
},
160+
}
161+
162+
function parseGiveCommand(reader: StringReader) {
163+
if (reader.peek() === '/') {
164+
reader.skip()
165+
}
166+
if (reader.peek() === 'g' && reader.peek(1) === 'i' && reader.peek(2) === 'v' && reader.peek(3) === 'e') {
167+
reader.cursor += 4
168+
reader.expect(' ')
169+
reader.expect('@')
170+
if (reader.peek().match(/[parsen]/)) {
171+
reader.skip()
172+
} else {
173+
throw reader.createError("Expected 'p', 'a', 'r', 's', 'e', or 'n'")
174+
}
175+
reader.expect(' ')
176+
}
177+
const item = parseIdentifier(reader)
178+
const components = parseComponents(reader)
179+
let count = 1
180+
if (reader.peek() === ' ') {
181+
reader.skip()
182+
count = reader.readInt()
183+
}
184+
return new ItemStack(item, count, components)
185+
}
186+
187+
188+
function parseComponents(reader: StringReader) {
189+
const components = new Map<string, NbtTag>()
190+
if (reader.peek() !== '[') {
191+
return components
192+
}
193+
reader.skip()
194+
reader.skipWhitespace()
195+
while (reader.peek() !== ']') {
196+
if (reader.peek() === '!') {
197+
reader.skip()
198+
reader.skipWhitespace()
199+
const key = parseIdentifier(reader)
200+
components.set('!' + key, new NbtCompound())
201+
reader.skipWhitespace()
202+
} else {
203+
const key = parseIdentifier(reader)
204+
reader.skipWhitespace()
205+
reader.expect('=')
206+
reader.skipWhitespace()
207+
const tag = NbtTag.fromString(reader)
208+
reader.skipWhitespace()
209+
if (key.is('custom_data')) {
210+
components.set(key.toString(), new NbtString(tag.toString()))
211+
} else {
212+
components.set(key.toString(), tag)
213+
}
214+
}
215+
if (reader.peek() === ']') {
216+
break
217+
} else if (reader.peek() === ',') {
218+
reader.skip()
219+
reader.skipWhitespace()
220+
continue
221+
}
222+
throw reader.createError("Expected ',' or ']'")
223+
}
224+
reader.skip()
225+
return components
226+
}
227+
228+
function parseIdentifier(reader: StringReader) {
229+
const start = reader.cursor
230+
while (reader.canRead() && reader.peek().match(/[a-z0-9_.:\/-]/)) {
231+
reader.skip()
232+
}
233+
const result = reader.getRead(start)
234+
if (result.length === 0) {
235+
throw reader.createError('Expected a resource location')
236+
}
237+
return Identifier.parse(result)
238+
}
239+
240+
function createLootTable(item: ItemStack) {
241+
return {
242+
pools: [
243+
{
244+
rolls: 1,
245+
entries: [
246+
{
247+
type: 'minecraft:item',
248+
name: item.id.toString(),
249+
functions: (item.components.size > 0 || item.count > 1)
250+
? [
251+
...item.components.size > 0 ? [{
252+
function: 'minecraft:set_components',
253+
components: Object.fromEntries([...item.components.entries()].map(([key, value]) => {
254+
return [key, value.toSimplifiedJson()]
255+
})),
256+
}] : [],
257+
...item.count > 1 ? [{
258+
function: 'minecraft:set_count',
259+
count: item.count,
260+
}]: [],
261+
]
262+
: undefined,
263+
},
264+
],
265+
},
266+
],
267+
}
268+
}
269+
270+
function getItemFromLootTable(data: unknown): ItemStack {
271+
const root = Json.readObject(data) ?? {}
272+
const pools = Json.readArray(root.pools, e => Json.readObject(e) ?? {}) ?? []
273+
if (pools.length === 0) {
274+
throw new Error('Expected a pool')
275+
}
276+
const pool = pools[0]
277+
const entries = Json.readArray(pool.entries, e => Json.readObject(e) ?? {}) ?? []
278+
if (entries.length === 0) {
279+
throw new Error('Expected an entry')
280+
}
281+
const entry = entries[0]
282+
const type = Json.readString(entry.type)
283+
if (type?.replace(/^minecraft:/, '') !== 'item') {
284+
throw new Error('Expected "type" to be "minecraft:item"')
285+
}
286+
const name = Json.readString(entry.name)
287+
if (!name) {
288+
throw new Error('Expected "name"')
289+
}
290+
const functions = [
291+
...Json.readArray(entry.functions, e => Json.readObject(e) ?? {}) ?? [],
292+
...Json.readArray(pool.functions, e => Json.readObject(e) ?? {}) ?? [],
293+
...Json.readArray(root.functions, e => Json.readObject(e) ?? {}) ?? [],
294+
]
295+
let count = 1
296+
const components = new Map<string, NbtTag>()
297+
for (const fn of functions) {
298+
const type = Json.readString(fn.function)?.replace(/^minecraft:/, '')
299+
switch (type) {
300+
case 'set_count':
301+
const value = Json.readInt(fn.count)
302+
if (value) {
303+
count = value
304+
}
305+
break
306+
case 'set_components':
307+
const newComponents = Json.readObject(fn.components) ?? {}
308+
for (const [key, value] of Object.entries(newComponents)) {
309+
components.set(key, jsonToNbt(value))
310+
}
311+
}
312+
}
313+
return new ItemStack(Identifier.parse(name), count, components)
314+
}
315+
316+
function stringifyItemStack(itemStack: ItemStack) {
317+
let result = itemStack.id.toString()
318+
if (itemStack.components.size > 0) {
319+
result += `[${[...itemStack.components.entries()].map(([k, v]) => {
320+
return k.startsWith('!') ? k : `${k}=${v.toString()}`
321+
}).join(',')}]`
322+
}
323+
if (itemStack.count > 1) {
324+
result += ` ${itemStack.count}`
325+
}
326+
return result
327+
}

src/app/pages/Home.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ function Tools() {
8787
const { locale } = useLocale()
8888

8989
return <ToolGroup title={locale('tools')}>
90+
<ToolCard title="Converter" icon="convert"
91+
link="/convert/"
92+
desc="Turn /give commands into loot tables" />
9093
<ToolCard title="Customized Worlds" icon="customized"
9194
link="/customized/"
9295
desc="Create data packs to customize your world" />
@@ -99,9 +102,6 @@ function Tools() {
99102
<ToolCard title="Transformation preview"
100103
link="/transformation/"
101104
desc="Visualize transformations for display entities" />
102-
<ToolCard title="Data Pack Upgrader"
103-
link="https://misode.github.io/upgrader/"
104-
desc="Convert your data packs from 1.16 to 1.20" />
105105
<ToolCard title="Template Placer"
106106
link="https://misode.github.io/template-placer/"
107107
desc="Automatically place all the structure pieces in your world" />

src/app/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './Changelog.js'
2+
export * from './Convert.jsx'
23
export * from './Customized.jsx'
34
export * from './Generator.js'
45
export * from './Generators.jsx'

0 commit comments

Comments
 (0)