mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
Add guides (#224)
* Add guides * Add versioning to guides * Guides: special variables and nested expressions * Add guides page to vite build * Add search and hash hooks, guide tags and headings * Improve guides list and filtering * Add 1.19 download link
This commit is contained in:
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -10,5 +10,9 @@
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"typescript.format.semicolons": "remove",
|
||||
"editor.insertSpaces": false,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true
|
||||
}
|
||||
|
||||
1191
package-lock.json
generated
1191
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@
|
||||
"deepslate": "^0.9.0-beta.13",
|
||||
"deepslate-1.18": "npm:deepslate@^0.9.0-beta.9",
|
||||
"deepslate-rs": "^0.1.6",
|
||||
"highlight.js": "^11.5.1",
|
||||
"howler": "^2.2.3",
|
||||
"js-yaml": "^3.14.1",
|
||||
"lz-string": "^1.4.4",
|
||||
@@ -51,10 +52,11 @@
|
||||
"@typescript-eslint/parser": "^4.25.0",
|
||||
"cypress": "^9.2.0",
|
||||
"eslint": "^7.27.0",
|
||||
"fast-glob": "^3.2.11",
|
||||
"preact": "^10.5.13",
|
||||
"preact-router": "^3.2.1",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"typescript": "^4.1.3",
|
||||
"vite": "^2.3.7"
|
||||
"vite": "^2.3.7",
|
||||
"vite-plugin-static-copy": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import '../styles/global.css'
|
||||
import '../styles/nodes.css'
|
||||
import { Analytics } from './Analytics'
|
||||
import { Header } from './components'
|
||||
import { Category, Changelog, Generator, Home, Project, Sounds, Versions } from './pages'
|
||||
import { Category, Changelog, Generator, Guide, Guides, Home, Project, Sounds, Versions } from './pages'
|
||||
import { cleanUrl } from './Utils'
|
||||
|
||||
export function App() {
|
||||
const changeRoute = (e: RouterOnChangeArgs) => {
|
||||
window.dispatchEvent(new CustomEvent('replacestate'))
|
||||
// Needs a timeout to ensure the title is set correctly
|
||||
setTimeout(() => Analytics.pageview(cleanUrl(e.url)))
|
||||
}
|
||||
@@ -23,6 +24,8 @@ export function App() {
|
||||
<Changelog path="/changelog" />
|
||||
<Versions path="/versions" />
|
||||
<Project path="/project" />
|
||||
<Guides path="/guides/" />
|
||||
<Guide path="/guides/:id" />
|
||||
<Generator default />
|
||||
</Router>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { Path } from '@mcschema/core'
|
||||
import { getCurrentUrl, route } from 'preact-router'
|
||||
import yaml from 'js-yaml'
|
||||
import { route } from 'preact-router'
|
||||
import rfdc from 'rfdc'
|
||||
import config from '../config.json'
|
||||
|
||||
@@ -8,7 +9,7 @@ export function isPromise(obj: any): obj is Promise<any> {
|
||||
return typeof (obj as any)?.then === 'function'
|
||||
}
|
||||
|
||||
export function isObject(obj: any) {
|
||||
export function isObject(obj: any): obj is Record<string, any> {
|
||||
return typeof obj === 'object' && obj !== null
|
||||
}
|
||||
|
||||
@@ -68,29 +69,79 @@ export function getGenerator(url: string) {
|
||||
return config.generators.find(g => g.url === trimmedUrl)
|
||||
}
|
||||
|
||||
export function getSearchParams(url: string) {
|
||||
const searchIndex = url.indexOf('?')
|
||||
if (searchIndex >= 0) {
|
||||
url = url.slice(searchIndex + 1)
|
||||
return new Map(url.split('&').map<[string, string]>(param => {
|
||||
const index = param.indexOf('=')
|
||||
if (index === -1) return [param, 'true']
|
||||
return [decodeURIComponent(param.slice(0, index)), decodeURIComponent(param.slice(index + 1))]
|
||||
}))
|
||||
}
|
||||
return new Map<string, string>()
|
||||
export function changeUrl({ path, search, hash, replace }: { path?: string, search?: string, hash?: string, replace?: boolean }) {
|
||||
const url = (path !== undefined ? cleanUrl(path) : location.pathname)
|
||||
+ (search !== undefined ? (search.startsWith('?') || search.length === 0 ? search : '?' + search) : location.search)
|
||||
+ (hash !== undefined ? (hash.startsWith('#') ? hash : '#' + hash) : location.hash)
|
||||
route(url, replace)
|
||||
}
|
||||
|
||||
export function setSeachParams(modifications: Record<string, string | undefined>, newPath?: string) {
|
||||
const url = getCurrentUrl()
|
||||
const searchParams = getSearchParams(url)
|
||||
Object.entries(modifications).forEach(([key, value]) => {
|
||||
if (value === undefined) searchParams.delete(key)
|
||||
else searchParams.set(key, value)
|
||||
})
|
||||
const search = Array.from(searchParams).map(([key, value]) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value).replaceAll('%2F', '/')}`)
|
||||
route(`${newPath ? cleanUrl(newPath) : getPath(url)}${search.length === 0 ? '' : `?${search.join('&')}`}`, true)
|
||||
export function parseFrontMatter(source: string): Record<string, any> {
|
||||
const data = yaml.load(source.substring(3, source.indexOf('---', 3)))
|
||||
if (!isObject(data)) return {}
|
||||
return data
|
||||
}
|
||||
|
||||
export function versionContent(content: string, version: string) {
|
||||
let cursor = 0
|
||||
while (true) {
|
||||
const start = content.indexOf('{#', cursor)
|
||||
if (start < 0) {
|
||||
break
|
||||
}
|
||||
const end = findMatchingClose(content, start + 2)
|
||||
const vStart = content.indexOf('#[', start + 1)
|
||||
let sub = ''
|
||||
if (vStart >= 0 && vStart < end) {
|
||||
const vEnd = content.indexOf(']', vStart + 2)
|
||||
const v = content.substring(vStart + 2, vEnd)
|
||||
if (v === version) {
|
||||
sub = content.substring(vEnd + 1, end).trim()
|
||||
}
|
||||
} else {
|
||||
const key = content.substring(start + 2, end)
|
||||
const versionConfig = config.versions.find(v => v.id === version)
|
||||
sub = ({
|
||||
version: versionConfig?.id,
|
||||
pack_format: versionConfig?.pack_format.toString(),
|
||||
} as Record<string, string | undefined>)[key] ?? ''
|
||||
}
|
||||
content = content.substring(0, start) + sub + content.substring(end + 2)
|
||||
cursor = start
|
||||
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
function findMatchingClose(source: string, index: number) {
|
||||
let depth = 0
|
||||
let iteration = 0
|
||||
while (iteration++ < 1000) {
|
||||
const close = source.indexOf('#}', index)
|
||||
const open = source.indexOf('{#', index)
|
||||
if (close < 0) {
|
||||
console.warn('Missing closing bracket')
|
||||
return source.length
|
||||
}
|
||||
if (open < 0) {
|
||||
if (depth === 0) {
|
||||
return close
|
||||
} else {
|
||||
depth -= 1
|
||||
index = close + 2
|
||||
}
|
||||
} else if (open < close) {
|
||||
depth += 1
|
||||
index = open + 2
|
||||
} else if (depth === 0) {
|
||||
return close
|
||||
} else {
|
||||
depth -= 1
|
||||
index = close + 2
|
||||
}
|
||||
}
|
||||
console.warn('Exceeded max iterations while finding closing bracket')
|
||||
return source.length
|
||||
}
|
||||
|
||||
export function stringToColor(str: string): [number, number, number] {
|
||||
|
||||
@@ -9,7 +9,7 @@ export function Giscus({ term }: Props) {
|
||||
const themeSuffix = actualTheme === 'light' ? '-burn' : ''
|
||||
const themeUrl = (import.meta as any).env.DEV
|
||||
? `http://localhost:3000/src/styles/giscus${themeSuffix}.css`
|
||||
: `https://${location.host}/assets/giscus${themeSuffix}.css`
|
||||
: `${location.protocol}//${location.host}/assets/giscus${themeSuffix}.css`
|
||||
|
||||
return <GiscusReact
|
||||
repo="misode/misode.github.io"
|
||||
|
||||
25
src/app/components/GuideCard.tsx
Normal file
25
src/app/components/GuideCard.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ChangelogTag } from './versions'
|
||||
|
||||
interface Props {
|
||||
title: string,
|
||||
link: string,
|
||||
versions: string[],
|
||||
tags: string[],
|
||||
activeTags?: string[],
|
||||
toggleTag?: (tag: string) => unknown,
|
||||
}
|
||||
export function GuideCard({ title, link, versions, tags, activeTags, toggleTag }: Props) {
|
||||
const onToggleTag = (tag: string) => (e: MouseEvent) => {
|
||||
if (toggleTag) toggleTag(tag)
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
return <a class="guide-card" href={link} >
|
||||
<span class="guide-versions">{versions.join(' • ')}</span>
|
||||
<h3>{title}</h3>
|
||||
<div class="guide-tags">
|
||||
{tags.sort().map(tag => <ChangelogTag label={tag} onClick={onToggleTag(tag)} active={activeTags?.includes(tag)} />)}
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from './ErrorPanel'
|
||||
export * from './forms'
|
||||
export * from './generator'
|
||||
export * from './Giscus'
|
||||
export * from './GuideCard'
|
||||
export * from './Header'
|
||||
export * from './Icons'
|
||||
export * from './Octicon'
|
||||
|
||||
@@ -56,9 +56,9 @@ export function ChangelogList({ changes, defaultOrder }: Props) {
|
||||
</div>}
|
||||
<div class="changelog-list">
|
||||
{sortedChangelogs === undefined
|
||||
? <span>{locale('loading')}</span>
|
||||
? <span class="note">{locale('loading')}</span>
|
||||
: sortedChangelogs.length === 0
|
||||
? <span>{locale('changelog.no_results')}</span>
|
||||
? <span class="note">{locale('changelog.no_results')}</span>
|
||||
: sortedChangelogs.map(change =>
|
||||
<ChangelogEntry change={change} activeTags={tags} toggleTag={toggleTag} />)}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { hashString } from '../../Utils'
|
||||
type TagProps = {
|
||||
label: string,
|
||||
active?: boolean,
|
||||
onClick?: () => unknown,
|
||||
onClick?: (e: MouseEvent) => unknown,
|
||||
}
|
||||
export function ChangelogTag({ label, active, onClick }: TagProps) {
|
||||
const color = label === 'breaking' ? 5 : hashString(label) % 360
|
||||
|
||||
@@ -22,7 +22,7 @@ export function VersionList({ versions, link }: Props) {
|
||||
|
||||
return <>
|
||||
<div class="versions-controls">
|
||||
<TextInput class="btn btn-input version-search" list="sound-list" placeholder={locale('versions.search')}
|
||||
<TextInput class="btn btn-input version-search" placeholder={locale('versions.search')}
|
||||
value={search} onChange={setSearch} />
|
||||
<Checkbox label="Include snapshots" value={snapshots} onChange={setSnapshots} />
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function TitleProvider({ children }: { children: ComponentChildren }) {
|
||||
|
||||
const changeTitle = useCallback((title: string, versions?: VersionId[]) => {
|
||||
versions ??= config.versions.map(v => v.id as VersionId)
|
||||
const titleVersions = versions.slice(versions.length - VERSIONS_IN_TITLE)
|
||||
const titleVersions = versions.slice(-VERSIONS_IN_TITLE)
|
||||
document.title = `${title} Minecraft ${titleVersions.join(', ')}`
|
||||
setTitle(title)
|
||||
}, [])
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import type { ComponentChildren } from 'preact'
|
||||
import { createContext } from 'preact'
|
||||
import { getCurrentUrl } from 'preact-router'
|
||||
import { useCallback, useContext, useEffect, useState } from 'preact/hooks'
|
||||
import { Analytics } from '../Analytics'
|
||||
import { useSearchParam } from '../hooks'
|
||||
import type { VersionId } from '../services'
|
||||
import { VersionIds } from '../services'
|
||||
import { Store } from '../Store'
|
||||
import { getSearchParams, setSeachParams } from '../Utils'
|
||||
|
||||
const VERSION_PARAM = 'version'
|
||||
|
||||
interface Version {
|
||||
version: VersionId,
|
||||
changeVersion: (version: VersionId, store?: boolean) => unknown,
|
||||
changeVersion: (version: VersionId, store?: boolean, updateSearch?: boolean) => unknown,
|
||||
changeTargetVersion: (version: VersionId, replace?: boolean) => unknown,
|
||||
}
|
||||
const Version = createContext<Version>({
|
||||
version: '1.18.2',
|
||||
changeVersion: () => {},
|
||||
changeTargetVersion: () => {},
|
||||
})
|
||||
|
||||
export function useVersion() {
|
||||
@@ -26,28 +27,29 @@ export function useVersion() {
|
||||
export function VersionProvider({ children }: { children: ComponentChildren }) {
|
||||
const [version, setVersion] = useState<VersionId>(Store.getVersion())
|
||||
|
||||
const searchParams = getSearchParams(getCurrentUrl())
|
||||
const targetVersion = searchParams.get(VERSION_PARAM)
|
||||
const [targetVersion, changeTargetVersion] = useSearchParam(VERSION_PARAM)
|
||||
|
||||
useEffect(() => {
|
||||
if (VersionIds.includes(targetVersion as VersionId) && version !== targetVersion) {
|
||||
setVersion(targetVersion as VersionId)
|
||||
}
|
||||
}, [version, targetVersion])
|
||||
|
||||
const changeVersion = useCallback((version: VersionId, store = true) => {
|
||||
if (getSearchParams(getCurrentUrl()).has(VERSION_PARAM)) {
|
||||
setSeachParams({ version })
|
||||
const changeVersion = useCallback((newVersion: VersionId, store = true, updateSearch = false) => {
|
||||
if (updateSearch || targetVersion) {
|
||||
changeTargetVersion(newVersion, true)
|
||||
}
|
||||
if (store) {
|
||||
Analytics.setVersion(version)
|
||||
Store.setVersion(version)
|
||||
Analytics.setVersion(newVersion)
|
||||
Store.setVersion(newVersion)
|
||||
}
|
||||
setVersion(version)
|
||||
}, [])
|
||||
setVersion(newVersion)
|
||||
}, [targetVersion])
|
||||
|
||||
const value: Version = {
|
||||
version,
|
||||
changeVersion,
|
||||
changeTargetVersion,
|
||||
}
|
||||
|
||||
return <Version.Provider value={value}>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './useActiveTimout'
|
||||
export * from './useCanvas'
|
||||
export * from './useFocus'
|
||||
export * from './useHash'
|
||||
export * from './useMediaQuery'
|
||||
export * from './useModel'
|
||||
export * from './useSearchParam'
|
||||
|
||||
27
src/app/hooks/useHash.ts
Normal file
27
src/app/hooks/useHash.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import { changeUrl } from '../Utils'
|
||||
|
||||
export function useHash(): [string, (hash: string) => unknown] {
|
||||
const [hash, setHash] = useState(window.location.hash)
|
||||
|
||||
const onChange = useCallback(() => {
|
||||
setHash(window.location.hash)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('hashchange', onChange)
|
||||
window.addEventListener('replacestate', onChange)
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', onChange)
|
||||
window.removeEventListener('replacestate', onChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const changeHash = useCallback((newHash: string) => {
|
||||
if (newHash !== hash) {
|
||||
changeUrl({ hash: newHash })
|
||||
}
|
||||
}, [hash])
|
||||
|
||||
return [hash, changeHash]
|
||||
}
|
||||
39
src/app/hooks/useSearchParam.ts
Normal file
39
src/app/hooks/useSearchParam.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||
import { changeUrl } from '../Utils'
|
||||
|
||||
const getValue = (search: string, param: string) => new URLSearchParams(search).get(param) ?? undefined
|
||||
|
||||
export function useSearchParam(param: string): [string | undefined, (value: string | undefined, replace?: boolean) => unknown] {
|
||||
const location = window.location
|
||||
const [value, setValue] = useState<string | undefined>(getValue(location.search, param))
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
setValue(getValue(location.search, param))
|
||||
}
|
||||
|
||||
window.addEventListener('popstate', onChange)
|
||||
window.addEventListener('pushstate', onChange)
|
||||
window.addEventListener('replacestate', onChange)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('popstate', onChange)
|
||||
window.removeEventListener('pushstate', onChange)
|
||||
window.removeEventListener('replacestate', onChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const changeValue = useCallback((newValue: string | undefined, replace?: boolean) => {
|
||||
if (newValue !== value) {
|
||||
const params = new URLSearchParams(location.search)
|
||||
if (newValue === undefined || newValue.length === 0) {
|
||||
params.delete(param)
|
||||
} else {
|
||||
params.set(param, newValue)
|
||||
}
|
||||
changeUrl({ search: params.toString().replaceAll('%7C', '|'), replace })
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return [value, changeValue]
|
||||
};
|
||||
@@ -1,23 +1,23 @@
|
||||
import { DataModel, Path } from '@mcschema/core'
|
||||
import { getCurrentUrl, route } from 'preact-router'
|
||||
import { useEffect, useErrorBoundary, useMemo, useState } from 'preact/hooks'
|
||||
import { useEffect, useErrorBoundary, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Analytics } from '../Analytics'
|
||||
import { Ad, Btn, BtnMenu, ErrorPanel, HasPreview, Octicon, PreviewPanel, SearchList, SourcePanel, TextInput, Tree } from '../components'
|
||||
import { useLocale, useProject, useTitle, useVersion } from '../contexts'
|
||||
import { useActiveTimeout, useModel } from '../hooks'
|
||||
import { useActiveTimeout, useModel, useSearchParam } from '../hooks'
|
||||
import { getOutput } from '../schema/transformOutput'
|
||||
import type { BlockStateRegistry, VersionId } from '../services'
|
||||
import { checkVersion, fetchPreset, getBlockStates, getCollections, getModel, getSnippet, shareSnippet, SHARE_KEY } from '../services'
|
||||
import { Store } from '../Store'
|
||||
import { cleanUrl, deepEqual, getGenerator, getSearchParams, message, setSeachParams } from '../Utils'
|
||||
import { cleanUrl, deepEqual, getGenerator } from '../Utils'
|
||||
|
||||
interface Props {
|
||||
default?: true,
|
||||
}
|
||||
export function Generator({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion } = useVersion()
|
||||
const { version, changeVersion, changeTargetVersion } = useVersion()
|
||||
const { project, file, updateFile, openFile, closeFile } = useProject()
|
||||
const [error, setError] = useState<Error | string | null>(null)
|
||||
const [errorBoundary, errorRetry] = useErrorBoundary()
|
||||
@@ -31,9 +31,12 @@ export function Generator({}: Props) {
|
||||
return <main><ErrorPanel error={`Cannot find generator "${getCurrentUrl()}"`} /></main>
|
||||
}
|
||||
|
||||
const allowedVersions = config.versions
|
||||
.filter(v => checkVersion(v.id, gen.minVersion, gen.maxVersion))
|
||||
.map(v => v.id as VersionId)
|
||||
const allowedVersions = useMemo(() => {
|
||||
return config.versions
|
||||
.filter(v => checkVersion(v.id, gen.minVersion, gen.maxVersion))
|
||||
.map(v => v.id as VersionId)
|
||||
.reverse()
|
||||
}, [gen.minVersion, gen.maxVersion])
|
||||
|
||||
useTitle(locale('title.generator', locale(gen.id)), allowedVersions)
|
||||
|
||||
@@ -44,14 +47,15 @@ export function Generator({}: Props) {
|
||||
setError(`This generator is not available in versions above ${gen.maxVersion}`)
|
||||
}
|
||||
|
||||
const searchParams = getSearchParams(getCurrentUrl())
|
||||
const currentPreset = searchParams.get('preset')
|
||||
const sharedSnippetId = searchParams.get(SHARE_KEY)
|
||||
const [currentPreset, setCurrentPreset] = useSearchParam('preset')
|
||||
const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY)
|
||||
const ignoreChange = useRef(false)
|
||||
useEffect(() => {
|
||||
if (model && currentPreset) {
|
||||
loadPreset(currentPreset).then(preset => {
|
||||
ignoreChange.current = true
|
||||
model.reset(DataModel.wrapLists(preset), false)
|
||||
setSeachParams({ version, preset: currentPreset, [SHARE_KEY]: undefined })
|
||||
setSharedSnippetId(undefined)
|
||||
})
|
||||
} else if (model && sharedSnippetId) {
|
||||
getSnippet(sharedSnippetId).then(s => loadSnippet(model, s))
|
||||
@@ -107,11 +111,15 @@ export function Generator({}: Props) {
|
||||
|
||||
const [dirty, setDirty] = useState(false)
|
||||
useModel(model, () => {
|
||||
setSeachParams({ version: undefined, preset: undefined, [SHARE_KEY]: undefined })
|
||||
if (!ignoreChange.current) {
|
||||
setCurrentPreset(undefined, true)
|
||||
setSharedSnippetId(undefined, true)
|
||||
}
|
||||
ignoreChange.current = false
|
||||
Store.setBackup(gen.id, DataModel.unwrapLists(model?.data))
|
||||
setError(null)
|
||||
setDirty(true)
|
||||
}, [gen.id])
|
||||
}, [gen.id, setCurrentPreset, setSharedSnippetId])
|
||||
|
||||
const [fileRename, setFileRename] = useState('')
|
||||
const [fileSaved, doSave] = useActiveTimeout()
|
||||
@@ -211,7 +219,9 @@ export function Generator({}: Props) {
|
||||
|
||||
const selectPreset = (id: string) => {
|
||||
Analytics.generatorEvent('load-preset', id)
|
||||
setSeachParams({ version, preset: id, [SHARE_KEY]: undefined })
|
||||
setSharedSnippetId(undefined, true)
|
||||
changeTargetVersion(version, true)
|
||||
setCurrentPreset(id)
|
||||
}
|
||||
|
||||
const loadPreset = async (id: string) => {
|
||||
@@ -226,12 +236,13 @@ export function Generator({}: Props) {
|
||||
}
|
||||
return preset
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e : message(e))
|
||||
setError(`Cannot load preset ${id} in ${version}`)
|
||||
setCurrentPreset(undefined, true)
|
||||
}
|
||||
}
|
||||
|
||||
const selectVersion = (version: VersionId) => {
|
||||
setSeachParams({ [SHARE_KEY]: undefined })
|
||||
setSharedSnippetId(undefined, true)
|
||||
changeVersion(version)
|
||||
}
|
||||
|
||||
@@ -244,13 +255,13 @@ export function Generator({}: Props) {
|
||||
return
|
||||
}
|
||||
if (currentPreset) {
|
||||
setShareUrl(`${location.protocol}//${location.host}/${gen.url}/?version=${version}&preset=${currentPreset}`)
|
||||
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.protocol}//${location.host}/${gen.url}/?version=${version}`)
|
||||
setShareUrl(`${location.origin}/${gen.url}/?version=${version}`)
|
||||
setShareShown(true)
|
||||
} else {
|
||||
shareSnippet(gen.id, version, output, previewShown)
|
||||
@@ -345,7 +356,7 @@ export function Generator({}: Props) {
|
||||
<SearchList searchPlaceholder={locale('search')} noResults={locale('no_presets')} values={presets} onSelect={selectPreset}/>
|
||||
</BtnMenu>
|
||||
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')} data-cy="version-switcher">
|
||||
{allowedVersions.reverse().map(v =>
|
||||
{allowedVersions.map(v =>
|
||||
<Btn label={v} active={v === version} onClick={() => selectVersion(v)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
|
||||
147
src/app/pages/Guide.tsx
Normal file
147
src/app/pages/Guide.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import hljs from 'highlight.js'
|
||||
import { marked } from 'marked'
|
||||
import { route } from 'preact-router'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Ad, Btn, BtnMenu, ChangelogTag, Giscus, Octicon } from '../components'
|
||||
import { useLocale, useTitle, useVersion } from '../contexts'
|
||||
import { useActiveTimeout, useHash } from '../hooks'
|
||||
import type { VersionId } from '../services'
|
||||
import { parseFrontMatter, versionContent } from '../Utils'
|
||||
|
||||
const HASH = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.368 1.01a.75.75 0 01.623.859L6.57 4.5h3.98l.46-2.868a.75.75 0 011.48.237L12.07 4.5h2.18a.75.75 0 010 1.5h-2.42l-.64 4h2.56a.75.75 0 010 1.5h-2.8l-.46 2.869a.75.75 0 01-1.48-.237l.42-2.632H5.45l-.46 2.869a.75.75 0 01-1.48-.237l.42-2.632H1.75a.75.75 0 010-1.5h2.42l.64-4H2.25a.75.75 0 010-1.5h2.8l.46-2.868a.75.75 0 01.858-.622zM9.67 10l.64-4H6.33l-.64 4h3.98z"></path></svg>'
|
||||
|
||||
marked.use({
|
||||
highlight: (code, lang) => {
|
||||
if (lang === '') return undefined
|
||||
return hljs.highlight(code, { language: lang }).value
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
path?: string
|
||||
id?: string
|
||||
}
|
||||
export function Guide({ id }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion } = useVersion()
|
||||
const { changeTitle } = useTitle()
|
||||
|
||||
const [content, setContent] = useState<string | undefined>(undefined)
|
||||
|
||||
const frontMatter = useMemo(() => {
|
||||
if (!content) return undefined
|
||||
const data = parseFrontMatter(content)
|
||||
changeTitle(data?.title, data?.versions)
|
||||
return data
|
||||
}, [content])
|
||||
|
||||
const allowedVersions = useMemo(() => {
|
||||
const orderedVersions = config.versions.map(v => v.id)
|
||||
return (frontMatter?.versions as string[])
|
||||
?.sort((a, b) => orderedVersions.indexOf(b) - orderedVersions.indexOf(a))
|
||||
}, [frontMatter?.versions])
|
||||
|
||||
const guideVersion = useMemo(() => {
|
||||
if (!allowedVersions) return version
|
||||
if (allowedVersions.includes(version)) return version
|
||||
return allowedVersions[0]
|
||||
}, [version, frontMatter?.versions])
|
||||
|
||||
const versionedContent = useMemo(() => {
|
||||
if (!content) return undefined
|
||||
const guide = content.substring(content.indexOf('---', 3) + 3)
|
||||
return versionContent(guide, guideVersion)
|
||||
}, [guideVersion, content])
|
||||
|
||||
const html = useMemo(() => {
|
||||
if (!versionedContent) return undefined
|
||||
marked.use({ renderer: {
|
||||
link(href, title, text) {
|
||||
if (href === null) return text
|
||||
const title2 = title ? ` title="${title}"` : ''
|
||||
const target = href?.match(/^https?:\/\//) ? ' target="_blank"' : ''
|
||||
return `<a href="${href}"${title2}${target}>${text}</a>`
|
||||
},
|
||||
heading(text, level, raw, slugger) {
|
||||
const id = slugger.slug(raw)
|
||||
const link = `<span id="guide-${id}" href="?version=${version}#${id}">${HASH}</span>`
|
||||
return `<h${level}>${link}${text}</h${level}>`
|
||||
},
|
||||
}})
|
||||
return marked(versionedContent, { version: '1.19' } as any)
|
||||
}, [versionedContent])
|
||||
|
||||
const [hash, setHash] = useHash()
|
||||
|
||||
const scrollToHeading = useCallback(() => {
|
||||
if (!html) return
|
||||
const heading = document.querySelector(`[id=guide-${hash.slice(1)}]`)
|
||||
if (heading) {
|
||||
const top = heading.getBoundingClientRect().top + window.scrollY
|
||||
window.scrollTo({ top: top - 68, behavior: 'smooth' })
|
||||
}
|
||||
}, [html, hash])
|
||||
|
||||
useEffect(() => {
|
||||
scrollToHeading()
|
||||
}, [html === undefined, hash])
|
||||
|
||||
const clickGuideContent = useCallback((e: MouseEvent) => {
|
||||
if (!(e.target instanceof HTMLSpanElement)) return
|
||||
const targetHash = '#' + e.target.id.replace(/^guide-/, '')
|
||||
changeVersion(version, false, true)
|
||||
setHash(targetHash)
|
||||
if (targetHash === hash) {
|
||||
scrollToHeading()
|
||||
}
|
||||
}, [scrollToHeading, hash, version])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch(`../../guides/${id}.md`)
|
||||
const text = await res.text()
|
||||
setContent(text)
|
||||
})()
|
||||
}, [id])
|
||||
|
||||
const [shareActive, shareSuccess] = useActiveTimeout()
|
||||
|
||||
const onShare = useCallback(() => {
|
||||
const url = `${location.origin}/guides/${id}/?version=${version}`
|
||||
navigator.clipboard.writeText(url)
|
||||
shareSuccess()
|
||||
}, [id, version])
|
||||
|
||||
const onClickTag = (tag: string) => {
|
||||
route(`/guides/?tags=${tag}`)
|
||||
}
|
||||
|
||||
const [largeWidth] = useState(window.innerWidth > 600)
|
||||
|
||||
return <main>
|
||||
<div class="guide">
|
||||
<div class="navigation">
|
||||
<a class="btn btn-link" href="/guides/">
|
||||
{Octicon.arrow_left}
|
||||
{locale('guides.all')}
|
||||
</a>
|
||||
<Btn icon={shareActive ? 'check' : 'link'} label={locale('share')} onClick={onShare} active={shareActive} tooltip={locale(shareActive ? 'copied' : 'copy_share')} class="guide-share" />
|
||||
{allowedVersions && <BtnMenu icon="tag" label={guideVersion} tooltip={locale('switch_version')}>
|
||||
{allowedVersions.map((v: string) =>
|
||||
<Btn label={v} active={v === guideVersion} onClick={() => changeVersion(v as VersionId)} />)}
|
||||
</BtnMenu>}
|
||||
</div>
|
||||
{(frontMatter?.tags && frontMatter.tags.length > 0) && <div class="guide-tags">
|
||||
{frontMatter.tags.map((tag: string) =>
|
||||
<ChangelogTag label={tag} active onClick={() => onClickTag(tag)} />
|
||||
)}
|
||||
</div>}
|
||||
{html && <>
|
||||
<Ad id="guide" type={largeWidth ? 'image' : 'text'} />
|
||||
<div class="guide-content" dangerouslySetInnerHTML={{ __html: html }} onClick={clickGuideContent}></div>
|
||||
<Giscus />
|
||||
</>}
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
87
src/app/pages/Guides.tsx
Normal file
87
src/app/pages/Guides.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Btn, BtnMenu, ChangelogTag, GuideCard, TextInput } from '../components'
|
||||
import { useLocale, useTitle, useVersion } from '../contexts'
|
||||
import { useSearchParam } from '../hooks'
|
||||
import type { VersionId } from '../services'
|
||||
|
||||
interface Guide {
|
||||
id: string,
|
||||
title: string,
|
||||
versions?: string[],
|
||||
tags?: string[],
|
||||
}
|
||||
|
||||
declare var __GUIDES__: Guide[]
|
||||
|
||||
const TAG_KEY = 'tags'
|
||||
const TAG_SEP = '|'
|
||||
|
||||
interface Props {
|
||||
path?: string
|
||||
}
|
||||
export function Guides({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion } = useVersion()
|
||||
useTitle(locale('title.guides'))
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [tags, setTags] = useSearchParam(TAG_KEY)
|
||||
const activeTags = useMemo(() => tags?.split(TAG_SEP) ?? [], [tags])
|
||||
const toggleTag = (tag: string) => {
|
||||
if (activeTags.includes(tag)) {
|
||||
setTags(activeTags.filter(t => t !== tag).join(TAG_SEP))
|
||||
} else {
|
||||
setTags([...activeTags, tag].sort().join(TAG_SEP))
|
||||
}
|
||||
}
|
||||
|
||||
const [versionFilter, setVersionFiler] = useState(false)
|
||||
|
||||
const versionedGuides = useMemo(() => {
|
||||
if (versionFilter === false) return __GUIDES__
|
||||
return __GUIDES__.filter(guide => {
|
||||
return guide.versions?.includes(version)
|
||||
})
|
||||
}, [version, versionFilter])
|
||||
|
||||
const filteredGuides = useMemo(() => {
|
||||
const query = search.split(' ').map(q => q.trim().toLowerCase()).filter(q => q.length > 0)
|
||||
return versionedGuides.filter(guide => {
|
||||
if (!activeTags.every(tag => guide.tags?.includes(tag))) {
|
||||
return false
|
||||
}
|
||||
const content = guide.tags?.join(' ') + ' ' + guide.title.toLowerCase()
|
||||
return query.every(q => {
|
||||
if (q.startsWith('!')) {
|
||||
return q.length === 1 || !content.includes(q.slice(1))
|
||||
}
|
||||
return content.includes(q)
|
||||
})
|
||||
})
|
||||
}, [versionedGuides, search, tags])
|
||||
|
||||
return <main>
|
||||
<div class="guides">
|
||||
<div class="changelog-query">
|
||||
<TextInput class="btn btn-input changelog-search" placeholder={locale('guides.search')} value={search} onChange={setSearch} />
|
||||
<BtnMenu icon="tag" label={versionFilter ? version : locale('any_version')} tooltip={locale('switch_version')}>
|
||||
<Btn label={locale('any_version')} active={!versionFilter} onClick={() => setVersionFiler(!versionFilter)} />
|
||||
{config.versions.slice().reverse().map(v =>
|
||||
<Btn label={v.id} active={versionFilter && v.id === version} onClick={() => {changeVersion(v.id as VersionId); setVersionFiler(true)}} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
</div>
|
||||
{activeTags.length > 0 && <div class="changelog-tags">
|
||||
{activeTags.map(tag => <ChangelogTag label={tag} onClick={() => toggleTag(tag)} />)}
|
||||
</div>}
|
||||
{versionedGuides.length === 0 ? <>
|
||||
<span class="note">{locale('guides.no_results.version')}</span>
|
||||
</> : filteredGuides.length === 0 ? <>
|
||||
<span class="note">{locale('guides.no_results.query')}</span>
|
||||
</> : filteredGuides.map(g =>
|
||||
<GuideCard title={g.title} link={`/guides/${g.id}/`} tags={g.tags ?? []} versions={g.versions ?? []} activeTags={activeTags} toggleTag={toggleTag} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export function Home({}: Props) {
|
||||
desc="Convert your data packs from 1.16 to 1.17 to 1.18" />
|
||||
<ToolCard title="Technical Changelog" link="/changelog/" />
|
||||
<ToolCard title="Minecraft Versions" link="/versions/" />
|
||||
<ToolCard title="Data Pack Guides" link="/guides/" />
|
||||
<Giscus />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -64,8 +64,8 @@ export function Sounds({}: Props) {
|
||||
{configs.length > 1 && <Btn icon="play" label={ locale('sounds.play_all')} class="play-all-sounds" onClick={playAll} />}
|
||||
<div class="spacer"></div>
|
||||
<Btn icon="download" label={locale('download')} tooltip={locale('sounds.download_function')} class="download-sounds" onClick={downloadFunction} />
|
||||
<BtnMenu icon="tag" label={version}>
|
||||
{config.versions.reverse().map(v =>
|
||||
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')}>
|
||||
{config.versions.slice().reverse().map(v =>
|
||||
<Btn label={v.id} active={v.id === version} onClick={() => changeVersion(v.id as VersionId)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { getCurrentUrl } from 'preact-router'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { Ad, ErrorPanel, Octicon, VersionDetail, VersionList } from '../components'
|
||||
import { useLocale, useTitle } from '../contexts'
|
||||
import { useSearchParam } from '../hooks'
|
||||
import type { VersionMeta } from '../services'
|
||||
import { fetchVersions } from '../services'
|
||||
import { getSearchParams } from '../Utils'
|
||||
|
||||
interface Props {
|
||||
path?: string,
|
||||
@@ -21,7 +20,7 @@ export function Versions({}: Props) {
|
||||
.catch(e => { console.error(e); setError(e) })
|
||||
}, [])
|
||||
|
||||
const selectedId = getSearchParams(getCurrentUrl()).get('id')
|
||||
const [selectedId] = useSearchParam('id')
|
||||
const selected = versions.find(v => v.id === selectedId)
|
||||
|
||||
useTitle(selected ? selected.name : 'Versions Explorer', selected ? [] : undefined)
|
||||
@@ -34,7 +33,7 @@ export function Versions({}: Props) {
|
||||
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
|
||||
<div class="versions">
|
||||
{selectedId ? <>
|
||||
<div class="version-navigation">
|
||||
<div class="navigation">
|
||||
<a class="btn btn-link" href="/versions/">
|
||||
{Octicon.three_bars}
|
||||
{locale('versions.all')}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from './Category'
|
||||
export * from './Changelog'
|
||||
export * from './Generator'
|
||||
export * from './Guide'
|
||||
export * from './Guides'
|
||||
export * from './Home'
|
||||
export * from './Project'
|
||||
export * from './Sounds'
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function fetchPreset(versionId: VersionId, registry: string, id: st
|
||||
const type = ['blockstates', 'models'].includes(registry) ? 'assets' : 'data'
|
||||
const url = `${mcmeta(version, type)}/${type}/minecraft/${registry}/${id}.json`
|
||||
const res = await fetch(url)
|
||||
return res.json()
|
||||
return await res.json()
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred while fetching ${registry} preset ${id}: ${message(e)}`)
|
||||
}
|
||||
|
||||
@@ -53,27 +53,33 @@
|
||||
"versions": [
|
||||
{
|
||||
"id": "1.15",
|
||||
"ref": "1.15.2"
|
||||
"ref": "1.15.2",
|
||||
"pack_format": 5
|
||||
},
|
||||
{
|
||||
"id": "1.16",
|
||||
"ref": "1.16.5"
|
||||
"ref": "1.16.5",
|
||||
"pack_format": 6
|
||||
},
|
||||
{
|
||||
"id": "1.17",
|
||||
"ref": "1.17.1"
|
||||
"ref": "1.17.1",
|
||||
"pack_format": 7
|
||||
},
|
||||
{
|
||||
"id": "1.18",
|
||||
"ref": "1.18.1"
|
||||
"ref": "1.18.1",
|
||||
"pack_format": 8
|
||||
},
|
||||
{
|
||||
"id": "1.18.2",
|
||||
"ref": "1.18.2"
|
||||
"ref": "1.18.2",
|
||||
"pack_format": 9
|
||||
},
|
||||
{
|
||||
"id": "1.19",
|
||||
"dynamic": true
|
||||
"dynamic": true,
|
||||
"pack_format": 10
|
||||
}
|
||||
],
|
||||
"generators": [
|
||||
|
||||
161
src/guides/adding-custom-structures.md
Normal file
161
src/guides/adding-custom-structures.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
title: Adding custom structures
|
||||
versions:
|
||||
- '1.18.2'
|
||||
- '1.19'
|
||||
tags:
|
||||
- worldgen
|
||||
- structures
|
||||
---
|
||||
|
||||
This guide will showcase how to create a data pack that adds a custom structure to the world. There is also a [data pack download]({#[1.18.2] https://gist.github.com/misode/45559d34627755ecaa52497daea83544/raw/8ece848257e6ce17769ca17eccdf89b5889afbe2/tall-towers-1.18.2.zip #}{#[1.19] https://gist.github.com/misode/45559d34627755ecaa52497daea83544/raw/b7d7c44a132641d308cbdc93ce4cf061759d15c5/tall-towers-1.19.zip #}) of this complete example.
|
||||
|
||||
> **Always leave the world and rejoin to apply the new changes!**
|
||||
|
||||
## Pack.mcmeta
|
||||
Like every data pack, we need a `pack.mcmeta`. In this version, the pack format is {#pack_format#}.
|
||||
```json
|
||||
{
|
||||
"pack": {
|
||||
"pack_format": {#pack_format#},
|
||||
"description": "A tall tower"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## The structure set
|
||||
A structure set is where the placement starts. It defines where in the world the structure should be placed, and how rare it is. It takes a weighted list of different structures, allowing structure variants (for example the [vanilla nether](/worldgen/structure-set/?preset=nether_complexes&version={#version#}) has a structure set with both the bastion and fortress).
|
||||
|
||||
**`data/example/worldgen/structure_set/tall_towers.json`**
|
||||
```json
|
||||
{
|
||||
"structures": [
|
||||
{
|
||||
"structure": "example:tall_tower",
|
||||
"weight": 1
|
||||
}
|
||||
],
|
||||
"placement": {
|
||||
"type": "minecraft:random_spread",
|
||||
"spacing": 5,
|
||||
"separation": 2,
|
||||
"salt": 1646207470
|
||||
}
|
||||
}
|
||||
```
|
||||
Structure sets are made up of two parts:
|
||||
* `structures`: A weighted list of configured structure features [(see next step)](#the{#[1.18.2] -configured #}-structure).
|
||||
* `placement`: The structure placement
|
||||
* `placement.type`: Either `random_spread` or `concentric_rings`. The latter is only used by strongholds in vanilla, so we'll focus on `random_spread`
|
||||
* `placement.spacing`: Roughly the average distance in chunks between two structures in this set.
|
||||
* `placement.separation`: The minimum distance in chunks. Needs to be smaller than spacing.
|
||||
* `placement.salt`: A random number that is combined with the world seed. Always use a different random number for different structures, otherwise they will end up being placed in the same spot!
|
||||
|
||||
When using the `random_spread` placement type, it generates structures grid-based. Here's an illustration of the above example with `spacing = 5`, `separation = 2`. There will be one structure attempt in each 5x5 chunk grid, and only at `X` a structure can spawn.
|
||||
```
|
||||
.............
|
||||
..XXX..XXX..X
|
||||
..XXX..XXX..X
|
||||
..XXX..XXX..X
|
||||
.............
|
||||
.............
|
||||
..XXX..XXX..X
|
||||
..XXX..XXX..X
|
||||
..XXX..XXX..X
|
||||
```
|
||||
|
||||
## The {#[1.18.2] configured structure #}{#[1.19] structure #}
|
||||
The {#[1.18.2] configured structure (feature) #}{#[1.19] structure #} is the ID you will be able to reference in `/locate`.
|
||||
|
||||
**`data/example/worldgen/{#[1.18.2] configured_structure_feature #}{#[1.19] structure #}/tall_tower.json`**
|
||||
```json
|
||||
{#[1.18.2]
|
||||
{
|
||||
"type": "minecraft:village",
|
||||
"config": {
|
||||
"start_pool": "example:tall_tower",
|
||||
"size": 1
|
||||
},
|
||||
"biomes": "#minecraft:has_structure/mineshaft",
|
||||
"adapt_noise": true,
|
||||
"spawn_overrides": {}
|
||||
}
|
||||
#}{#[1.19]
|
||||
{
|
||||
"type": "minecraft:jigsaw",
|
||||
"biomes": "#minecraft:has_structure/mineshaft",
|
||||
"step": "surface_structures",
|
||||
"spawn_overrides": {},
|
||||
"terrain_adaptation": "beard_thin",
|
||||
"start_pool": "example:tall_tower",
|
||||
"size": 1,
|
||||
"start_height": {
|
||||
"absolute": 0
|
||||
},
|
||||
"project_start_to_heightmap": "WORLD_SURFACE_WG",
|
||||
"max_distance_from_center": 80,
|
||||
"use_expansion_hack": false
|
||||
}
|
||||
#}
|
||||
```
|
||||
Let's go over all the fields.
|
||||
{#[1.18.2]
|
||||
* `type`: This is the structure feature type. When making custom structures, you almost always want to set this to `village` or `bastion_remnant`. There is one important difference between the two: using `village` will spawn the structure on the surface, while `bastion_remnant` will always spawn the structure at Y=33.
|
||||
* `config.start_pool`: This is a reference to the **template pool** [(see next step)](#the-template-pool).
|
||||
* `config.size`: This is a number between 1 and 7. This is important if your structure uses jigsaw. In this simple example, we'll leave it at 1.
|
||||
* `biomes`: This controls in which biomes this structure is allowed to generate. You can give it any biome tag, a list of biomes, or a single biome. For easy testing we'll set it to every biome with mineshafts.
|
||||
* `adapt_noise`: When true, it will add extra terrain below each structure piece.
|
||||
* `spawn_overrides`: This field allows you to override mob spawning inside the structure bounding boxes. This is outside the scope of this guide, but you could look at the [vanilla monument](/worldgen/structure-feature/?preset=monument&version={#version#}) structure feature as a reference.
|
||||
#}{#[1.19]
|
||||
* `type`: This is the structure type. When making custom structures, you almost always want to set this to `jigsaw`.
|
||||
* `biomes`: This controls in which biomes this structure is allowed to generate. You can give it any biome tag, a list of biomes, or a single biome. For easy testing we'll set it to every biome with mineshafts.
|
||||
* `step`: The generation step to place the features in. This matches the steps in a biome's `feature` list. Possible values: `raw_generation`, `lakes`, `local_modifications`, `underground_structures`, `surface_structures`, `strongholds`, `underground_ores`, `underground_decoration`, `fluid_springs`, `vegetal_decoration`, and `top_layer_modification`.
|
||||
* `spawn_overrides`: This field allows you to override mob spawning inside the structure bounding boxes. This is outside the scope of this guide, but you could look at the [vanilla monument](/worldgen/structure/?preset=monument&version={#version#}) structure feature as a reference.
|
||||
* `start_pool`: This is a reference to the **template pool** [(see next step)](#the-template-pool).
|
||||
* `size`: This is a number between 1 and 7. This is important if your structure uses jigsaw. In this simple example, we'll leave it at 1.
|
||||
* `start_height`: A height provider specifying at which height the structure should spawn. The example uses the constant shorthand so it just specifies a vertical anchor.
|
||||
* `project_start_to_heightmap`: An optional heightmap type. Possible values: `WORLD_SURFACE_WG`, `WORLD_SURFACE`, `OCEAN_FLOOR_WG`, `OCEAN_FLOOR`, `MOTION_BLOCKING`, and `MOTION_BLOCKING_NO_LEAVES`. If `start_height` is not 0, will move the start relative to the heightmap.
|
||||
* `max_distance_from_center`: Value between 1 and 128. The maximum distance that a jigsaw can branch out.
|
||||
* `use_expansion_hack`: You should always set this to false. Vanilla villages set this to true to fix an issue with their streets.
|
||||
#}
|
||||
|
||||
## The template pool
|
||||
The template pool defines how to build up your structure. Since we're not using jigsaw, this is quite straight forward: we want to place a single NBT structure.
|
||||
|
||||
**`data/example/worldgen/template_pool/tall_tower.json`**
|
||||
```json
|
||||
{
|
||||
"name": "example:tall_tower",
|
||||
"fallback": "minecraft:empty",
|
||||
"elements": [
|
||||
{
|
||||
"weight": 1,
|
||||
"element": {
|
||||
"element_type": "minecraft:single_pool_element",
|
||||
"location": "example:stone_tall_tower",
|
||||
"projection": "rigid",
|
||||
"processors": "minecraft:empty"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Again, let's go over the fields:
|
||||
* `name`: For some reason, the game needs the name of this template pool. Just set this to the ID of the template pool.
|
||||
* `fallback`: Used in jigsaw structures, but we can simply use `minecraft:empty`.
|
||||
* `elements`: A weighted list of pool elements to choose from. You can add multiple elements here if your structure has different starting structure files. For example in vanilla a plains village has different town center variants.
|
||||
* `element_type`: The type of this element. One of `empty_pool_element` (placing nothing), `feature_pool_element` (placing a placed feature), `legacy_single_pool_element`, `list_pool_element`, and `single_pool_element` (placing a structure).
|
||||
* `location`: The path to the structure NBT file. [(see next step)](#the-structure-nbt).
|
||||
* `projection`: Either `rigid` or `terrain_matching`. Use the latter if you want the structure to match the terrain, just like village paths do.
|
||||
* `processors`: If you want to run any processor lists, this is quite complicated so again we'll skip this for now and set it to `minecraft:empty`.
|
||||
|
||||
## The structure NBT
|
||||
Creating the structure NBT file is entirely up to you. In this example I'm going to use a tower structure from [Gamemode 4](https://gm4.co/modules/tower-structures).
|
||||
|
||||
**`data/example/structures/stone_tall_tower.nbt`**
|
||||
|
||||
(binary NBT file) [Download the structure from this example](https://gist.github.com/misode/45559d34627755ecaa52497daea83544/raw/8b41b3e273210e0455e4bd4fa97b5504b65aff2c/stone_tall_tower.nbt)
|
||||
|
||||
## Result
|
||||

|
||||

|
||||
@@ -3,6 +3,7 @@
|
||||
"add_bottom": "Add to bottom",
|
||||
"add_top": "Add to top",
|
||||
"advancement": "Advancement",
|
||||
"any_version": "Any",
|
||||
"assets": "Assets",
|
||||
"block_definition": "Blockstate",
|
||||
"changelog.search": "Search changes",
|
||||
@@ -34,6 +35,10 @@
|
||||
"fields": "Fields",
|
||||
"generate_new_seed": "Generate new seed",
|
||||
"github": "GitHub",
|
||||
"guides.all": "All guides",
|
||||
"guides.search": "Search guides",
|
||||
"guides.no_results.version": "No guides for this version",
|
||||
"guides.no_results.query": "No guides for this query",
|
||||
"hide_output": "Hide output",
|
||||
"hide_preview": "Hide preview",
|
||||
"home": "Home",
|
||||
@@ -80,6 +85,7 @@
|
||||
"title.changelog": "Technical Changelog",
|
||||
"title.generator": "%0% Generator",
|
||||
"title.generator_category": "%0% Generators",
|
||||
"title.guides": "Data Pack Guides",
|
||||
"title.home": "Data Pack Generators",
|
||||
"title.project": "%0% Project",
|
||||
"title.sounds": "Sound Explorer",
|
||||
|
||||
@@ -808,7 +808,7 @@ main.has-preview {
|
||||
color: var(--text-1)
|
||||
}
|
||||
|
||||
.home, .category, .project, .versions {
|
||||
.home, .category, .project, .versions, .guides, .guide {
|
||||
padding: 16px;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
@@ -882,6 +882,10 @@ hr {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.note {
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
.settings {
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -1386,11 +1390,11 @@ hr {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.version-navigation {
|
||||
.navigation {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.version-navigation > *:not(:last-child) {
|
||||
.navigation > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@@ -1496,6 +1500,168 @@ hr {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.guide-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: var(--text-2);
|
||||
background-color: var(--background-2);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.guide-versions {
|
||||
color: var(--text-3);
|
||||
float: right;
|
||||
}
|
||||
|
||||
.guide {
|
||||
padding-left: 32px;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.guide .navigation > :first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.guide-share.active {
|
||||
fill: var(--accent-success);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.guide-tags {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.guide .ad {
|
||||
margin: 12px 0 8px;
|
||||
}
|
||||
|
||||
.guide .ad[data-ea-type="image"] {
|
||||
float: right;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.guide-content {
|
||||
color: var(--text-2);
|
||||
margin-top: 12px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.guide-content p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.guide-content h1,
|
||||
.guide-content h2,
|
||||
.guide-content h3,
|
||||
.guide-content h4,
|
||||
.guide-content h5,
|
||||
.guide-content h6 {
|
||||
margin: 1.2em 0 0.4em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.guide-content h1 > [id],
|
||||
.guide-content h2 > [id],
|
||||
.guide-content h3 > [id],
|
||||
.guide-content h4 > [id],
|
||||
.guide-content h5 > [id],
|
||||
.guide-content h6 > [id] {
|
||||
fill: var(--text-3);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
float: left;
|
||||
padding-right: 4px;
|
||||
margin-left: -20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.guide-content h1 > [id] *,
|
||||
.guide-content h2 > [id] *,
|
||||
.guide-content h3 > [id] *,
|
||||
.guide-content h4 > [id] *,
|
||||
.guide-content h5 > [id] *,
|
||||
.guide-content h6 > [id] * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.guide-content h1:hover > [id],
|
||||
.guide-content h2:hover > [id],
|
||||
.guide-content h3:hover > [id],
|
||||
.guide-content h4:hover > [id],
|
||||
.guide-content h5:hover > [id],
|
||||
.guide-content h6:hover > [id] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.guide-content blockquote {
|
||||
border-left: 4px solid var(--background-6);
|
||||
color: var(--text-3);
|
||||
padding-left: 0.7em;
|
||||
margin: 0.7em 0;
|
||||
}
|
||||
|
||||
.guide-content blockquote > p {
|
||||
padding: 0.3em 0;
|
||||
}
|
||||
|
||||
.guide-content a {
|
||||
text-decoration: underline;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.guide-content ul, .guide-content ol {
|
||||
padding-left: 1.6em;
|
||||
margin: 0.2em 0 0.5em;
|
||||
}
|
||||
|
||||
.guide-content li + li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.guide-content img {
|
||||
border-radius: 0.2em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.guide-content code {
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.4em;
|
||||
margin: 0;
|
||||
background-color: var(--background-2);
|
||||
border-radius: 6px;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.guide-content pre > code {
|
||||
display: block;
|
||||
padding: 0.8em;
|
||||
margin: 0.2em 0 0.5em;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.guide-content code .hljs-attr {
|
||||
color: var(--editor-variable);
|
||||
}
|
||||
|
||||
.guide-content code .hljs-string {
|
||||
color: var(--editor-string);
|
||||
}
|
||||
|
||||
.guide-content code .hljs-number {
|
||||
color: var(--editor-number);
|
||||
}
|
||||
|
||||
.guide-content code .hljs-keyword {
|
||||
color: var(--editor-constant);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
.sound-search-group {
|
||||
margin-bottom: 8px;
|
||||
@@ -1623,4 +1789,8 @@ hr {
|
||||
.version-metadata-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.guide-versions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
import preact from '@preact/preset-vite'
|
||||
import alias from '@rollup/plugin-alias'
|
||||
import html from '@rollup/plugin-html'
|
||||
import glob from 'fast-glob'
|
||||
import fs from 'fs'
|
||||
import yaml from 'js-yaml'
|
||||
import { env } from 'process'
|
||||
import copy from 'rollup-plugin-copy'
|
||||
import { defineConfig } from 'vite'
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
import config from './src/config.json'
|
||||
import English from './src/locales/en.json'
|
||||
|
||||
const guides = glob.sync('src/guides/**/*.md').flatMap(g => {
|
||||
const content = fs.readFileSync(g).toString('utf-8')
|
||||
if (!content.startsWith('---')) return []
|
||||
try {
|
||||
const frontMatter = yaml.load(content.substring(3, content.indexOf('---', 3)))
|
||||
|
||||
if (typeof frontMatter !== 'object') return []
|
||||
return [{
|
||||
id: g.replace('src/guides/', '').replace('.md', ''),
|
||||
...frontMatter,
|
||||
}]
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
sourcemap: true,
|
||||
@@ -23,31 +42,29 @@ export default defineConfig({
|
||||
html({
|
||||
fileName: '404.html',
|
||||
title: '404',
|
||||
template: template,
|
||||
template,
|
||||
}),
|
||||
...['sounds', 'changelog', 'versions'].map(id => html({
|
||||
...['sounds', 'changelog', 'versions', 'guides'].map(id => html({
|
||||
fileName: `${id}/index.html`,
|
||||
title: getTitle({ id: `title.${id}`, page: true }),
|
||||
template: template,
|
||||
template,
|
||||
})),
|
||||
...['worldgen', 'assets'].map(id => html({
|
||||
fileName: `${id}/index.html`,
|
||||
title: getTitle({ id, category: true }),
|
||||
template: template,
|
||||
template,
|
||||
})),
|
||||
...config.generators.map(m => html({
|
||||
fileName: `${m.url}/index.html`,
|
||||
title: getTitle(m),
|
||||
template: template,
|
||||
template,
|
||||
})),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'src/sitemap.txt', dest: 'dist' },
|
||||
{ src: 'src/sitemap.txt', dest: 'dist', rename: 'sitemap2.txt' },
|
||||
{ src: 'src/styles/giscus.css', dest: 'dist/assets' },
|
||||
{ src: 'src/styles/giscus-burn.css', dest: 'dist/assets' },
|
||||
],
|
||||
hook: 'writeBundle',
|
||||
...guides.map(g => {
|
||||
return html({
|
||||
fileName: `guides/${g.id}/index.html`,
|
||||
title: `${g.title} Minecraft${g.versions ? ` ${g.versions.join(' ')}` : ''}`,
|
||||
template,
|
||||
})
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -57,8 +74,19 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
__LATEST_VERSION__: env.latest_version,
|
||||
__GUIDES__: guides,
|
||||
},
|
||||
plugins: [preact()],
|
||||
plugins: [
|
||||
preact(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: 'src/sitemap.txt', dest: '' },
|
||||
{ src: 'src/styles/giscus.css', dest: 'assets' },
|
||||
{ src: 'src/styles/giscus-burn.css', dest: 'assets' },
|
||||
{ src: 'src/guides/*', dest: 'guides' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
function getTitle(m) {
|
||||
|
||||
Reference in New Issue
Block a user