Merge pull request #614 from misode/mcdoc

This commit is contained in:
Misode
2024-11-27 16:57:33 +01:00
committed by GitHub
86 changed files with 5951 additions and 4274 deletions

2226
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,38 +9,27 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .ts,.tsx"
"lint": "eslint . --ext .ts,.tsx",
"postinstall": "patch-package"
},
"keywords": [],
"author": "Misode",
"license": "MIT",
"dependencies": {
"@giscus/react": "^2.2.3",
"@mcschema/core": "^0.13.0",
"@mcschema/java-1.15": "^0.2.13",
"@mcschema/java-1.16": "^0.6.20",
"@mcschema/java-1.17": "^0.2.40",
"@mcschema/java-1.18": "^0.3.16",
"@mcschema/java-1.18.2": "^0.1.26",
"@mcschema/java-1.19": "^0.1.54",
"@mcschema/java-1.19.3": "^0.0.17",
"@mcschema/java-1.19.4": "^0.1.21",
"@mcschema/java-1.20": "^0.0.24",
"@mcschema/java-1.20.2": "^0.0.15",
"@mcschema/java-1.20.3": "^0.0.16",
"@mcschema/java-1.20.5": "^0.0.42",
"@mcschema/java-1.21": "^0.0.27",
"@mcschema/java-1.21.2": "^0.0.16",
"@mcschema/java-1.21.4": "^0.0.15",
"@mcschema/locales": "^0.1.104",
"@spyglassmc/core": "^0.4.15",
"@spyglassmc/java-edition": "^0.3.19",
"@spyglassmc/json": "^0.3.17",
"@spyglassmc/locales": "^0.3.9",
"@spyglassmc/mcdoc": "^0.3.18",
"@spyglassmc/nbt": "^0.3.18",
"@zip.js/zip.js": "^2.4.5",
"brace": "^0.11.1",
"buffer": "^6.0.3",
"comment-json": "^4.1.1",
"deepslate": "^0.22.3",
"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",
"deepslate": "^0.22.3",
"diff": "^7.0.0",
"highlight.js": "^11.5.1",
"howler": "^2.2.3",
@@ -48,7 +37,9 @@
"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",
@@ -61,10 +52,12 @@
"@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",
"eslint": "^8.17.0",
"patch-package": "^8.0.0",
"postcss": "^8.4.31",
"preact": "^10.8.0",
"preact-router": "^3.2.1",

View File

@@ -0,0 +1,44 @@
diff --git a/node_modules/@spyglassmc/mcdoc/lib/runtime/checker/index.js b/node_modules/@spyglassmc/mcdoc/lib/runtime/checker/index.js
index 2d51735..70ac4a0 100644
--- a/node_modules/@spyglassmc/mcdoc/lib/runtime/checker/index.js
+++ b/node_modules/@spyglassmc/mcdoc/lib/runtime/checker/index.js
@@ -545,9 +545,9 @@ function simplifyReference(typeDef, context) {
context.ctx.logger.warn(`Tried to access unknown reference ${typeDef.path}`);
return { typeDef: { kind: 'union', members: [] } };
}
- if (data.simplifiedTypeDef) {
- return { typeDef: data.simplifiedTypeDef };
- }
+ // if (data.simplifiedTypeDef) {
+ // return { typeDef: data.simplifiedTypeDef };
+ // }
const simplifiedResult = simplify(data.typeDef, context);
if (typeDef.attributes?.length) {
simplifiedResult.typeDef = {
@@ -555,16 +555,16 @@ function simplifyReference(typeDef, context) {
attributes: [...typeDef.attributes, ...simplifiedResult.typeDef.attributes ?? []],
};
}
- if (!simplifiedResult.dynamicData) {
- symbol.amend({
- data: {
- data: {
- ...data,
- simplifiedTypeDef: simplifiedResult.typeDef,
- },
- },
- });
- }
+ // if (!simplifiedResult.dynamicData) {
+ // symbol.amend({
+ // data: {
+ // data: {
+ // ...data,
+ // simplifiedTypeDef: simplifiedResult.typeDef,
+ // },
+ // },
+ // });
+ // }
return simplifiedResult;
}
function simplifyDispatcher(typeDef, context) {

View File

@@ -0,0 +1,116 @@
use ::java::server::util::direction::Direction
use ::java::server::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 {}

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

@@ -0,0 +1,179 @@
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]),
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]),
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_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_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_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],
}

View File

@@ -1,7 +1,7 @@
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 {
@@ -226,61 +226,44 @@ export namespace Analytics {
})
}
export function showProject(file_type: string, projects_count: number, project_size: number, method: Method) {
export function showProject(method: Method) {
event(ID_GENERATOR, 'show-project', legacyMethod(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) {
export function hideProject(method: Method) {
event(ID_GENERATOR, 'hide-project', legacyMethod(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) {
export function saveProjectFile(method: Method) {
event(ID_GENERATOR, 'save-project-file', legacyMethod(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) {
export function deleteProjectFile(method: Method) {
event(ID_GENERATOR, 'delete-project-file', legacyMethod(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) {
export function renameProjectFile(method: Method) {
event(ID_GENERATOR, 'rename-project-file', legacyMethod(method))
gtag('event', 'rename_project_file', {
file_type,
projects_count,
project_size,
method,
})
}
export function deleteProject(projects_count: number, project_size: number, method: Method) {
export function deleteProject(method: Method) {
event(ID_GENERATOR, 'delete-project', legacyMethod(method))
gtag('event', 'delete_project', {
projects_count,
project_size,
method,
})
}

View File

@@ -1,5 +1,5 @@
import config from '../config.json'
import type { VersionId } from './services/Schemas.js'
import type { VersionId } from './services/Versions.js'
export interface ConfigLanguage {
code: string,
@@ -20,11 +20,10 @@ export interface ConfigVersion {
export interface ConfigGenerator {
id: string,
url: string,
schema: string,
path?: string,
noPath?: boolean,
tags?: string[],
partner?: 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,5 +1,3 @@
import type { DataModel } from '@mcschema/core'
import { Path } from '@mcschema/core'
import * as zip from '@zip.js/zip.js'
import type { Identifier, NbtTag, Random } from 'deepslate'
import { Matrix3, Matrix4, NbtByte, NbtCompound, NbtDouble, NbtInt, NbtList, NbtString, Vector } from 'deepslate'
@@ -32,7 +30,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() {
@@ -47,21 +49,6 @@ 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)
}
export function htmlEncode(str: string) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;')
@@ -308,23 +295,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()
}
@@ -640,3 +627,11 @@ export function makeDescriptionId(prefix: string, id: Identifier | undefined) {
}
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 } 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) {
@@ -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

@@ -22,9 +22,9 @@ export function Header() {
return <header>
<div class="title">
<Link class="home-link" href="/" aria-label={locale('home')} data-cy="home-link">{Icons.home}</Link>
<Link class="home-link" href="/" aria-label={locale('home')}>{Icons.home}</Link>
<h1 class="font-bold">{title}</h1>
{gen && <BtnMenu icon="chevron_down" tooltip={locale('switch_generator')} data-cy="generator-switcher">
{gen && <BtnMenu icon="chevron_down" tooltip={locale('switch_generator')}>
{config.generators
.filter(g => g.tags?.[0] === gen?.tags?.[0] && checkVersion(version, g.minVersion))
.map(g =>
@@ -39,7 +39,7 @@ export function Header() {
</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 +47,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}

View File

@@ -3,10 +3,9 @@ import { Identifier } from 'deepslate/core'
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 { fetchItemComponents, fetchRegistries } 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'
@@ -83,14 +82,17 @@ function ItemItself({ item }: ResolvedProps) {
return Octicon.package
}
const { value: collections } = useAsync(() => getCollections(version), [])
const { value: allModels, loading: loadingModels } = useAsync(async () => {
const registries = await fetchRegistries(version)
return registries.get('model')
}, [version])
if (collections === undefined) {
if (loadingModels || allModels === undefined) {
return null
}
const modelPath = `item/${item.id.path}`
if (collections.get('model').includes('minecraft:' + modelPath)) {
if (allModels && allModels.includes('minecraft:' + modelPath)) {
return <RenderedItem item={item} />
}

View File

@@ -3,8 +3,8 @@ 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 { getCollections } from '../services/Schemas.js'
import { ItemTooltip1204 } from './ItemTooltip1204.jsx'
import { Octicon } from './Octicon.jsx'
import { itemHasGlint } from './previews/LootTable1204.js'
@@ -69,14 +69,17 @@ function ItemItself({ item }: Props) {
return Octicon.package
}
const { value: collections } = useAsync(() => getCollections(version), [])
const { value: allModels, loading: loadingModels } = useAsync(async () => {
const registries = await fetchRegistries(version)
return registries.get('model')
}, [version])
if (collections === undefined) {
if (loadingModels || allModels === undefined) {
return null
}
const modelPath = `item/${item.id.path}`
if (collections.get('model').includes('minecraft:' + modelPath)) {
if (allModels && allModels.includes('minecraft:' + modelPath)) {
return <RenderedItem item={item} hasGlint={hasGlint} />
}

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

@@ -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

@@ -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,41 +1,57 @@
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 [error, setError] = useState<string>()
const { version } = useVersion()
const { hideModal } = useModal()
const { project } = useProject()
const { client } = useSpyglass()
const [fileId, setFileId] = useState(gen.id === 'pack_mcmeta' ? 'pack' : '')
const [error, setError] = useState<string>()
const changeFileId = (str: string) => {
setError(undefined)
setFileId(str)
}
const doSave = () => {
const doSave = useCallback(() => {
if (!fileId.match(/^([a-z0-9_.-]+:)?[a-z0-9/_.-]+$/)) {
setError('Invalid resource location')
return
}
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 id = Identifier.parse(fileId.includes(':') || project.namespace === undefined ? fileId : `${project.namespace}:${fileId}`)
const pack = gen.tags?.includes('assets') ? 'assets' : 'data'
const uri = `${getProjectRoot(project)}${pack}/${id.namespace}/${genPath(gen, version)}/${id.path}.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" onDismiss={onClose}>
return <Modal class="file-modal">
<p>{locale('project.save_current_file')}</p>
<TextInput autofocus={id !== 'pack_mcmeta'} class="btn btn-input" value={fileId} onChange={changeFileId} 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,40 +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 changeFileId = (str: string) => {
const changeFileId = useCallback((str: string) => {
setError(undefined)
setFileId(str)
}
}, [])
const doSave = () => {
const doRename = useCallback(() => {
if (!fileId.match(/^([a-z0-9_.-]+:)?[a-z0-9/_.-]+$/)) {
setError('Invalid resource location')
return
}
Analytics.renameProjectFile(id, projects.length, project.files.length, 'menu')
updateFile(id, name, { type: id, id: fileId })
onClose()
}
Analytics.renameProjectFile('menu')
onRename(fileId)
hideModal()
}, [fileId, hideModal])
return <Modal class="file-modal" onDismiss={onClose}>
return <Modal class="file-modal">
<p>{locale('project.rename_file')}</p>
<TextInput autofocus class="btn btn-input" value={fileId} onChange={changeFileId} onEnter={doSave} onCancel={onClose} placeholder={locale('resource_location')} spellcheck={false} />
<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={doSave} />
<Btn icon="pencil" label={locale('project.rename')} onClick={doRename} />
</Modal>
}

View File

@@ -0,0 +1,37 @@
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) {
return <ErrorPanel error={`Error viewing the file: ${message(error)}`} 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

@@ -2,8 +2,8 @@ import { useMemo } from 'preact/hooks'
import type { ConfigGenerator } from '../../Config.js'
import config from '../../Config.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'

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 {

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,485 @@
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::server::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_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',
'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',
'stat_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,69 +1,63 @@
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 { useDocAndNode } from '../../contexts/Spyglass.jsx'
import { useVersion } from '../../contexts/Version.jsx'
import { checkVersion } from '../../services/index.js'
import { safeJsonParse } from '../../Utils.js'
import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview, RecipePreview, StructureSetPreview } from '../previews/index.js'
export const HasPreview = ['loot_table', 'recipe', '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', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'worldgen/structure_set', 'block_definition', 'docAndNode']
type PreviewPanelProps = {
model: DataModel | undefined,
version: VersionId,
docAndNode: DocAndNode | undefined,
id: string,
shown: boolean,
onError: (message: string) => unknown,
}
export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) {
const [, setCount] = useState(0)
export function PreviewPanel({ docAndNode: original, id, shown }: PreviewPanelProps) {
const { version } = useVersion()
useModel(model, () => {
setCount(count => count + 1)
})
if (!original) return <></>
if (!model) return <></>
const data = model.get(new Path([]))
if (!data) return <></>
const docAndNode = useDocAndNode(original)
if (id === 'loot_table') {
return <LootTablePreview {...{ model, version, shown, data }} />
return <LootTablePreview {...{ docAndNode, shown }} />
}
if (id === 'recipe') {
return <RecipePreview {...{ model, version, shown, data }} />
return <RecipePreview {...{ docAndNode, shown }} />
}
if (id === 'dimension' && model.get(new Path(['generator', 'type']))?.endsWith('noise')) {
return <BiomeSourcePreview {...{ model, version, shown, data }} />
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 === '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,43 +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], version)
if (file) {
try {
const data = await parseSource(entry[1], 'json')
project.files!.push({ ...file, data })
return
} catch (e) {
console.warn(`Failed parsing ${file.type} ${file.id}: ${message(e)}`)
}
}
if (project.unknownFiles === undefined) {
project.unknownFiles = []
}
project.unknownFiles.push({ path: entry[0], data: entry[1] })
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())
@@ -80,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} />
@@ -90,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,27 @@
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')
const doSave = useCallback(() => {
Analytics.deleteProject('menu')
deleteProject(project.name)
onClose()
}
hideModal()
}, [deleteProject, hideModal])
return <Modal class="file-modal" onDismiss={onClose}>
return <Modal class="file-modal">
<p>{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,118 +1,103 @@
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 { disectFilePath, DRAFT_PROJECT, getFilePath, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { DRAFT_PROJECT, getProjectRoot, useLocale, useProject } 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 { Store } from '../../Store.js'
import { writeZip } from '../../Utils.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) {
export function ProjectPanel() {
const { locale } = useLocale()
const { version } = useVersion()
const { projects, project, changeProject, file, openFile, updateFile } = useProject()
const { showModal } = useModal()
const { projects, project, projectUri, setProjectUri, changeProject } = useProject()
const { client, service } = useSpyglass()
const [treeViewMode, setTreeViewMode] = useState(Store.getTreeViewMode())
const projectRoot = getProjectRoot(project)
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)
client.fs.readdir(projectRoot).then(entries => {
setEntries(entries.flatMap(e => {
return e.isFile() ? [e.name.slice(projectRoot.length)] : []
}))
})
}, [projectRoot])
useEffect(() => {
if (!service) {
return
}
return disectFilePath(entry, version)
}, [treeViewMode, version])
const entries = useMemo(() => project.files.flatMap(f => {
const path = getFilePath(f, version)
if (!path) return []
if (f.type === 'pack_mcmeta') return 'pack.mcmeta'
if (treeViewMode === 'resources') {
return [`${f.type.replaceAll('/', '\u2215')}/${f.id.replaceAll('/', '\u2215')}`]
}
return [path]
}), [treeViewMode, version, ...project.files])
const selected = useMemo(() => file && getFilePath(file, version), [file, version])
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, version)
if (path === undefined) return []
if (path === 'pack.mcmeta') hasPack = true
return [[path, stringifySource(file.data)]] as [string, string][]
})
project.unknownFiles?.forEach(({ path, data }) => {
entries.push([path, data])
})
if (!hasPack) {
const pack_format = config.versions.find(v => v.id === version)!.pack_format
entries.push(['pack.mcmeta', stringifySource({ pack: { pack_format, description: '' } })])
}
const url = await writeZip(entries)
if (!download.current || entries === undefined) return
const zipEntries = await Promise.all(entries.map(async e => {
const data = await client.fs.readFile(projectRoot + e)
return [e, data] as [string, Uint8Array]
}))
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} >
@@ -123,41 +108,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, version) === 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>
<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')} />
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreateProject} />
{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,23 @@
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 { 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 { checkVersion, fetchDependencyMcdoc, fetchPreset, fetchRegistries, getSnippet, shareSnippet } from '../../services/index.js'
import { DEPENDENCY_URI } from '../../services/Spyglass.js'
import { Store } from '../../Store.js'
import { cleanUrl, deepEqual, genPath } from '../../Utils.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js'
import { cleanUrl, genPath } from '../../Utils.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileView, Footer, HasPreview, Octicon, PreviewPanel, ProjectPanel, SearchList, 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,7 +26,9 @@ 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) {
@@ -32,25 +38,35 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
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,89 +89,111 @@ 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, project.name, 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 selectPreset = (id: string) => {
@@ -167,18 +205,11 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
const loadPreset = async (id: string) => {
try {
const preset = await fetchPreset(version, genPath(gen, version), 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
}
}
@@ -203,27 +234,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 = () => {
@@ -283,38 +308,61 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
}
}
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' : ''}`}>
<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={gen.wiki} target="_blank">
@@ -327,15 +375,16 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
<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} />
{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;`}>
@@ -356,10 +405,10 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
</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} onError={setError} />
</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 />
@@ -370,12 +419,9 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
{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,10 +1,10 @@
import { DataModel } from '@mcschema/core'
import type { DocAndNode } from '@spyglassmc/core'
import { fileUtil } from '@spyglassmc/core'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { useLocalStorage, useModel } from '../../hooks/index.js'
import { getOutput } from '../../schema/transformOutput.js'
import type { BlockStateRegistry } from '../../services/index.js'
import { getSourceFormats, getSourceIndent, getSourceIndents, parseSource, sortData, stringifySource } 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'
import { Btn, BtnMenu } from '../index.js'
@@ -17,17 +17,16 @@ interface Editor {
}
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 { service } = useSpyglass()
const [indent, setIndent] = useState(Store.getIndent())
const [format, setFormat] = useState(Store.getFormat())
const [sort, setSort] = useLocalStorage('misode_output_sort', 'schema')
@@ -40,20 +39,23 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
const textarea = useRef<HTMLTextAreaElement>(null)
const editor = useRef<Editor>()
const getSerializedOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
let data = getOutput(model, blockStates)
if (sort === 'alphabetically') {
data = sortData(data)
}
return stringifySource(data, format, indent)
const getSerializedOutput = useCallback((text: string) => {
// TODO: implement sort
// if (sort === 'alphabetically') {
// data = sortData(data)
// }
return stringifySource(text, format, indent)
}, [indent, format, sort])
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)
editor.current.setValue(output)
} catch (e) {
if (e instanceof Error) {
@@ -71,9 +73,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}`
@@ -84,7 +87,7 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
console.error(e)
}
}
}, [model, blockStates, indent, format, sort, highlighting])
}, [service, docAndNode, text, indent, format, sort, highlighting])
useEffect(() => {
if (highlighting) {
@@ -145,14 +148,11 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
}
}, [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
@@ -163,18 +163,18 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
}, [indent, format, sort, highlighting, braceLoaded])
useEffect(() => {
if (doCopy && model && blockStates) {
navigator.clipboard.writeText(getSerializedOutput(model, blockStates)).then(() => {
if (doCopy && text !== undefined) {
navigator.clipboard.writeText(getSerializedOutput(text)).then(() => {
copySuccess()
})
}
}, [doCopy])
}, [doCopy, text])
useEffect(() => {
if (doDownload && model && blockStates && download.current) {
const content = encodeURIComponent(getSerializedOutput(model, blockStates))
if (doDownload && docAndNode && text !== undefined && download.current) {
const content = encodeURIComponent(getSerializedOutput(text))
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()
}
@@ -215,7 +215,7 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
{window.matchMedia('(pointer: coarse)').matches && <>
<Btn icon="paste" onClick={importFromClipboard} />
</>}
<BtnMenu icon="gear" tooltip={locale('output_settings')} data-cy="source-controls">
<BtnMenu icon="gear" tooltip={locale('output_settings')}>
{getSourceIndents().map(key =>
<Btn label={locale(`indentation.${key}`)} active={indent === key}
onClick={() => changeIndent(key)}/>

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(DataModel.unwrapLists(data))
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))

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 }
}

View File

@@ -1,13 +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'
@@ -19,7 +18,7 @@ import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
const MODES = ['side', 'top', '3d'] as const
export const DensityFunctionPreview = ({ data, shown }: PreviewProps) => {
export const DensityFunctionPreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { project } = useProject()
const { version } = useVersion()
@@ -29,13 +28,15 @@ export const DensityFunctionPreview = ({ data, shown }: PreviewProps) => {
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>()

View File

@@ -3,7 +3,7 @@ import type { Random } from 'deepslate/core'
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/Schemas.js'
import type { VersionId } from '../../services/Versions.js'
import { clamp, getWeightedRandom, isObject, jsonToNbt } from '../../Utils.js'
export interface SlottedItem {
@@ -122,8 +122,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)
}
}
@@ -383,6 +386,9 @@ const LootFunctions: Record<string, (params: any) => LootFunction> = {
item.set('written_book_content', newContent)
},
set_components: ({ components }) => (item) => {
if (typeof components !== 'object' || components === null) {
return
}
for (const [key, value] of Object.entries(components)) {
item.set(key, jsonToNbt(value))
}
@@ -510,6 +516,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)
}

View File

@@ -1,7 +1,7 @@
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/Schemas.js'
import type { VersionId } from '../../services/Versions.js'
import { clamp, getWeightedRandom, isObject } from '../../Utils.js'
export interface SlottedItem {
@@ -117,8 +117,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)
}
}

View File

@@ -1,10 +1,9 @@
import { DataModel } from '@mcschema/core'
import { Identifier } from 'deepslate'
import { useMemo, useRef, useState } from 'preact/hooks'
import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import { checkVersion, fetchAllPresets, fetchItemComponents } from '../../services/index.js'
import { clamp, jsonToNbt, randomSeed } from '../../Utils.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'
@@ -12,7 +11,7 @@ import type { PreviewProps } from './index.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')
@@ -35,13 +34,13 @@ export const LootTablePreview = ({ data }: PreviewProps) => {
])
}, [version])
const table = DataModel.unwrapLists(data)
const state = JSON.stringify(table)
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,
@@ -61,7 +60,7 @@ export const LootTablePreview = ({ data }: PreviewProps) => {
getEnchantmentTag: (id) => (enchantmentTags?.get(id.replace(/^minecraft:/, '')) as any)?.values ?? [],
getBaseComponents: (id) => new Map([...(itemComponents?.get(Identifier.parse(id).toString()) ?? new Map()).entries()].map(([k, v]) => [k, jsonToNbt(v)])),
})
}, [version, seed, luck, daytime, weather, mixItems, state, dependencies, loading])
}, [version, seed, luck, daytime, weather, mixItems, text, dependencies, loading])
return <>
<div ref={overlay} class="preview-overlay">

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({ '': { 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(DataModel.unwrapLists(data))
model.flatten(resources)
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

@@ -1,19 +1,19 @@
import { DataModel } from '@mcschema/core'
import { Identifier, ItemStack } from 'deepslate'
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { useLocale, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/useAsync.js'
import type { VersionId } from '../../services/index.js'
import { checkVersion, fetchAllPresets } from '../../services/index.js'
import { jsonToNbt } from '../../Utils.js'
import { jsonToNbt, safeJsonParse } from '../../Utils.js'
import { Btn, BtnMenu } from '../index.js'
import { ItemDisplay } from '../ItemDisplay.jsx'
import type { PreviewProps } from './index.js'
const ANIMATION_TIME = 1000
export const RecipePreview = ({ data, version }: PreviewProps) => {
export const RecipePreview = ({ docAndNode }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const [advancedTooltips, setAdvancedTooltips] = useState(true)
const [animation, setAnimation] = useState(0)
const overlay = useRef<HTMLDivElement>(null)
@@ -29,14 +29,14 @@ export const RecipePreview = ({ data, version }: PreviewProps) => {
return () => clearInterval(interval)
}, [])
const recipe = DataModel.unwrapLists(data)
const state = JSON.stringify(recipe)
const text = docAndNode.doc.getText()
const recipe = safeJsonParse(text) ?? {}
const items = useMemo<Map<Slot, ItemStack>>(() => {
return placeItems(version, recipe, animation, itemTags ?? new Map())
}, [state, animation, itemTags])
}, [text, animation, itemTags])
const gui = useMemo(() => {
const type = recipe.type?.replace(/^minecraft:/, '')
const type = recipe?.type?.replace(/^minecraft:/, '')
if (type === 'smelting' || type === 'blasting' || type === 'smoking' || type === 'campfire_cooking') {
return '/images/furnace.png'
} else if (type === 'stonecutting') {
@@ -46,7 +46,7 @@ export const RecipePreview = ({ data, version }: PreviewProps) => {
} else {
return '/images/crafting_table.png'
}
}, [state])
}, [text])
return <>
<div ref={overlay} class="preview-overlay">

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 {

View File

@@ -1,5 +1,4 @@
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'
@@ -12,9 +11,7 @@ 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

@@ -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() {

View File

@@ -0,0 +1,40 @@
import type { ComponentChildren, FunctionComponent } from 'preact'
import { createContext } from 'preact'
import { useCallback, useContext, useState } from 'preact/hooks'
interface ModalContext {
showModal: (component: FunctionComponent) => void
hideModal: () => void
}
const ModalContext = createContext<ModalContext | undefined>(undefined)
export function useModal() {
const context = useContext(ModalContext)
if (context === undefined) {
throw new Error('Cannot use modal context')
}
return context
}
export function ModalProvider({ children }: { children: ComponentChildren }) {
const [modal, setModal] = useState<FunctionComponent>()
const showModal = useCallback((component: FunctionComponent) => {
setModal(component)
}, [])
const hideModal = useCallback(() => {
setModal(undefined)
}, [])
const value: ModalContext = {
showModal,
hideModal,
}
return <ModalContext.Provider value={value}>
{children}
{modal !== undefined && modal}
</ModalContext.Provider>
}

View File

@@ -1,155 +1,160 @@
import { Identifier } from 'deepslate'
import type { ComponentChildren } from 'preact'
import { createContext } from 'preact'
import { route } from 'preact-router'
import { useCallback, useContext, useMemo, useState } from 'preact/hooks'
import { useCallback, useContext, useState } from 'preact/hooks'
import type { ProjectData } from '../components/previews/Deepslate.js'
import config from '../Config.js'
import { useAsync } from '../hooks/useAsync.js'
import type { VersionId } from '../services/index.js'
import { DEFAULT_VERSION } from '../services/index.js'
import { DRAFTS_URI, PROJECTS_URI, SpyglassClient } from '../services/Spyglass.js'
import { Store } from '../Store.js'
import { cleanUrl, genPath } from '../Utils.js'
import { useVersion } from './Version.jsx'
import { genPath, hexId, message, safeJsonParse } from '../Utils.js'
export type Project = {
export type ProjectMeta = {
name: string,
namespace?: string,
version?: VersionId,
files: ProjectFile[],
storage?: ProjectStorage,
/** @deprecated */
files?: ProjectFile[],
/** @deprecated */
unknownFiles?: UnknownFile[],
}
export const DRAFT_PROJECT: Project = {
name: 'Drafts',
namespace: 'draft',
files: [],
export type ProjectStorage = {
type: 'indexeddb',
rootUri: string,
}
export type ProjectFile = {
type ProjectFile = {
type: string,
id: string,
data: any,
}
export type UnknownFile = {
type UnknownFile = {
path: string,
data: string,
}
export const FilePatterns = [
'worldgen/[a-z_]+',
'tags/worldgen/[a-z_]+',
'tags/[a-z_]+',
'[a-z_]+',
].map(e => RegExp(`^data/([a-z0-9._-]+)/(${e})/([a-z0-9/._-]+)$`))
export const DRAFT_PROJECT: ProjectMeta = {
name: 'Drafts',
namespace: 'draft',
storage: {
type: 'indexeddb',
rootUri: DRAFTS_URI,
},
}
interface ProjectContext {
projects: Project[],
project: Project,
file?: ProjectFile,
createProject: (name: string, namespace?: string, version?: VersionId) => unknown,
deleteProject: (name: string) => unknown,
changeProject: (name: string) => unknown,
updateProject: (project: Partial<Project>) => unknown,
updateFile: (type: string, id: string | undefined, file: Partial<ProjectFile>) => boolean,
openFile: (type: string, id: string) => unknown,
closeFile: () => unknown,
projects: ProjectMeta[],
project: ProjectMeta,
projectUri: string | undefined,
setProjectUri: (uri: string | undefined) => void,
createProject: (project: ProjectMeta) => void,
deleteProject: (name: string) => void,
changeProject: (name: string) => void,
updateProject: (project: Partial<ProjectMeta>) => void,
}
const Project = createContext<ProjectContext>({
projects: [DRAFT_PROJECT],
project: DRAFT_PROJECT,
createProject: () => {},
deleteProject: () => {},
changeProject: () => {},
updateProject: () => {},
updateFile: () => false,
openFile: () => {},
closeFile: () => {},
})
const Project = createContext<ProjectContext | undefined>(undefined)
export function useProject() {
return useContext(Project)
const context = useContext(Project)
if (context === undefined) {
throw new Error('Cannot use project outside of provider')
}
return context
}
export function ProjectProvider({ children }: { children: ComponentChildren }) {
const [projects, setProjects] = useState<Project[]>(Store.getProjects())
const { version } = useVersion()
const [projects, setProjects] = useState<ProjectMeta[]>(Store.getProjects())
const [openProject, setOpenProject] = useState<string>(Store.getOpenProject())
const [projectName, setProjectName] = useState<string>(Store.getOpenProject())
const project = useMemo(() => {
return projects.find(p => p.name === projectName) ?? DRAFT_PROJECT
}, [projects, projectName])
const { value: project} = useAsync(async () => {
const project = projects.find(p => p.name === openProject)
if (!project) {
if (openProject !== undefined && openProject !== DRAFT_PROJECT.name) {
console.warn(`Cannot find project ${openProject} to open`)
}
return DRAFT_PROJECT
}
if (project.storage === undefined) {
try {
const projectRoot = `${PROJECTS_URI}${hexId()}/`
console.log(`Upgrading project ${openProject} to IndexedDB at ${projectRoot}`)
await SpyglassClient.FS.mkdir(projectRoot)
if (project.files) {
await Promise.all(project.files.map(async file => {
const gen = config.generators.find(g => g.id === file.type)
if (!gen) {
console.warn(`Could not upgrade file ${file.id} of type ${file.type}, no generator found!`)
return
}
const type = genPath(gen, project.version ?? DEFAULT_VERSION)
const { namespace, path } = Identifier.parse(file.id)
const uri = type === 'pack_mcmeta'
? `${projectRoot}data/pack.mcmeta`
: `${projectRoot}data/${namespace}/${type}/${path}.json`
return SpyglassClient.FS.writeFile(uri, JSON.stringify(file.data, null, 2))
}))
}
if (project.unknownFiles) {
await Promise.all(project.unknownFiles.map(async file => {
const uri = projectRoot + file.path
return SpyglassClient.FS.writeFile(uri, file.data)
}))
}
changeProjects(projects.map(p => p === project ? { ...p, storage: { type: 'indexeddb', rootUri: projectRoot } } : p))
} catch (e) {
console.error(`Something went wrong upgrading project ${openProject}: ${message(e)}`)
return DRAFT_PROJECT
}
}
return project
}, [projects, openProject])
const [fileId, setFileId] = useState<[string, string] | undefined>(undefined)
const file = useMemo(() => {
if (!fileId) return undefined
return project.files.find(f => f.type === fileId[0] && f.id === fileId[1])
}, [project, fileId])
const [projectUri, setProjectUri] = useState<string>()
const changeProjects = useCallback((projects: Project[]) => {
const changeProjects = useCallback((projects: ProjectMeta[]) => {
Store.setProjects(projects)
setProjects(projects)
}, [])
const createProject = useCallback((name: string, namespace?: string, version?: VersionId) => {
changeProjects([...projects, { name, namespace, version, files: [] }])
const createProject = useCallback((project: ProjectMeta) => {
changeProjects([...projects, project])
}, [projects])
const deleteProject = useCallback((name: string) => {
const deleteProject = useCallback(async (name: string) => {
if (name === DRAFT_PROJECT.name) return
const project = projects.find(p => p.name === name)
if (project) {
const projectRoot = getProjectRoot(project)
const entries = await SpyglassClient.FS.readdir(projectRoot)
await Promise.all(entries.map(async e => SpyglassClient.FS.unlink(e.name)))
}
changeProjects(projects.filter(p => p.name !== name))
setOpenProject(DRAFT_PROJECT.name)
}, [projects])
const changeProject = useCallback((name: string) => {
Store.setOpenProject(name)
setProjectName(name)
setOpenProject(name)
}, [])
const updateProject = useCallback((edits: Partial<Project>) => {
changeProjects(projects.map(p => p.name === projectName ? { ...p, ...edits } : p))
}, [projects, projectName])
const updateFile = useCallback((type: string, id: string | undefined, edits: Partial<ProjectFile>) => {
if (!edits.id) { // remove
updateProject({ files: project.files.filter(f => f.type !== type || f.id !== id) })
} else {
const newId = type === 'pack_mcmeta' ? 'pack' : edits.id.includes(':') ? edits.id : `${project.namespace ?? 'minecraft'}:${edits.id}`
const exists = project.files.some(f => f.type === type && f.id === newId)
if (!id) { // create
if (exists) return false
updateProject({ files: [...project.files, { type, id: newId, data: edits.data ?? {} } ]})
setFileId([type, newId])
} else { // rename or update data
if (file?.id === id && id !== newId && exists) {
return false
}
updateProject({ files: project.files.map(f => f.type === type && f.id === id ? { ...f, ...edits, id: newId } : f)})
if (file?.id === id) setFileId([type, newId])
}
}
return true
}, [updateProject, project, file])
const openFile = useCallback((type: string, id: string) => {
const gen = config.generators.find(g => g.id === type || genPath(g, version) === type)
if (!gen) {
throw new Error(`Cannot find generator of type ${type}`)
}
setFileId([gen.id, id])
route(cleanUrl(gen.url))
}, [version])
const closeFile = useCallback(() => {
setFileId(undefined)
}, [])
const updateProject = useCallback((edits: Partial<ProjectMeta>) => {
changeProjects(projects.map(p => p.name === openProject ? { ...p, ...edits } : p))
}, [projects, openProject])
const value: ProjectContext = {
projects,
project,
file,
project: project ?? DRAFT_PROJECT,
projectUri,
setProjectUri,
createProject,
changeProject,
deleteProject,
updateProject,
updateFile,
openFile,
closeFile,
}
return <Project.Provider value={value}>
@@ -157,44 +162,30 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) {
</Project.Provider>
}
export function getFilePath(file: { id: string, type: string }, version: VersionId) {
const [namespace, id] = file.id.includes(':') ? file.id.split(':') : ['minecraft', file.id]
if (file.type === 'pack_mcmeta') {
if (file.id === 'pack') return 'pack.mcmeta'
return undefined
export function getProjectRoot(project: ProjectMeta) {
if (project.storage?.type === 'indexeddb') {
return project.storage.rootUri
}
const gen = config.generators.find(g => g.id === file.type)
if (!gen) {
return undefined
}
return `data/${namespace}/${genPath(gen, version)}/${id}.json`
throw new Error(`Unsupported project storage ${project.storage?.type}`)
}
export function disectFilePath(path: string, version: VersionId) {
if (path === 'pack.mcmeta') {
return { type: 'pack_mcmeta', id: 'pack' }
}
for (const p of FilePatterns) {
const match = path.match(p)
if (!match) continue
const gen = config.generators.find(g => (genPath(g, version) ?? g.id) === match[2])
if (!gen) continue
const namespace = match[1]
const name = match[3].replace(/\.[a-z]+$/, '')
return {
type: gen.id,
id: `${namespace}:${name}`,
export async function getWorldgenProjectData(project: ProjectMeta): Promise<ProjectData> {
const projectRoot = getProjectRoot(project)
const categories = ['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function']
const result: ProjectData = Object.fromEntries(categories.map(c => [c, {}]))
const entries = await SpyglassClient.FS.readdir(projectRoot)
for (const entry of entries) {
for (const category of categories) {
if (entry.name.includes(category)) {
const pattern = RegExp(`data/([a-z0-9_.-]+)/${category}/([a-z0-9_./-]+).json$`)
const match = entry.name.match(pattern)
if (match) {
const data = await SpyglassClient.FS.readFile(entry.name)
const text = new TextDecoder().decode(data)
result[category][`${match[1]}:${match[2]}`] = safeJsonParse(text)
}
}
}
}
return undefined
}
export function getProjectData(project: Project) {
return Object.fromEntries(['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function'].map(type => {
const resources = Object.fromEntries(
project.files.filter(file => file.type === type)
.map<[string, unknown]>(file => [file.id, file.data])
)
return [type, resources]
}))
return result
}

View File

@@ -0,0 +1,76 @@
import type { DocAndNode } from '@spyglassmc/core'
import type { ComponentChildren } from 'preact'
import { createContext } from 'preact'
import type { Inputs } from 'preact/hooks'
import { useContext, useEffect, useState } from 'preact/hooks'
import { useAsync } from '../hooks/useAsync.js'
import type { SpyglassService } from '../services/Spyglass.js'
import { SpyglassClient } from '../services/Spyglass.js'
import { useVersion } from './Version.jsx'
interface SpyglassContext {
client: SpyglassClient
service: SpyglassService | undefined
serviceLoading: boolean
}
const SpyglassContext = createContext<SpyglassContext | undefined>(undefined)
export function useSpyglass(): SpyglassContext {
const ctx = useContext(SpyglassContext)
if (ctx === undefined) {
throw new Error('Cannot use Spyglass context')
}
return ctx
}
export function watchSpyglassUri(
uri: string | undefined,
handler: (docAndNode: DocAndNode) => void,
inputs: Inputs = [],
) {
const { service } = useSpyglass()
useEffect(() => {
if (!uri || !service) {
return
}
service.watchFile(uri, handler)
return () => service.unwatchFile(uri, handler)
}, [service, uri, handler, ...inputs])
}
export function useDocAndNode(original: DocAndNode, inputs?: Inputs): DocAndNode
export function useDocAndNode(original: DocAndNode | undefined, inputs?: Inputs): DocAndNode | undefined
export function useDocAndNode(original: DocAndNode | undefined, inputs: Inputs = []) {
const [wrapped, setWrapped] = useState(original)
useEffect(() => {
setWrapped(original)
}, [original, setWrapped, ...inputs])
watchSpyglassUri(original?.doc.uri, updated => {
setWrapped(updated)
}, [original?.doc.uri, setWrapped, ...inputs])
return wrapped
}
export function SpyglassProvider({ children }: { children: ComponentChildren }) {
const { version } = useVersion()
const [client] = useState(new SpyglassClient())
const { value: service, loading: serviceLoading } = useAsync(() => {
return client.createService(version)
}, [client, version])
const value: SpyglassContext = {
client,
service,
serviceLoading,
}
return <SpyglassContext.Provider value={value}>
{children}
</SpyglassContext.Provider>
}

View File

@@ -3,6 +3,7 @@ import { createContext } from 'preact'
import { useCallback, useContext } from 'preact/hooks'
import { useLocalStorage } from '../hooks/index.js'
import type { Color } from '../Utils.js'
import { safeJsonParse } from '../Utils.js'
interface Store {
biomeColors: Record<string, [number, number, number]>
@@ -19,7 +20,7 @@ export function useStore() {
}
export function StoreProvider({ children }: { children: ComponentChildren }) {
const [biomeColors, setBiomeColors] = useLocalStorage<Record<string, Color>>('misode_biome_colors', {}, JSON.parse, JSON.stringify)
const [biomeColors, setBiomeColors] = useLocalStorage<Record<string, Color>>('misode_biome_colors', {}, s => safeJsonParse(s) ?? {}, JSON.stringify)
const setBiomeColor = useCallback((biome: string, color: Color) => {
setBiomeColors({...biomeColors, [biome]: color })

View File

@@ -5,6 +5,5 @@ export * from './useFocus.js'
export * from './useHash.js'
export * from './useLocalStorage.js'
export * from './useMediaQuery.js'
export * from './useModel.js'
export * from './useSearchParam.js'
export * from './useTags.js'

View File

@@ -1,20 +0,0 @@
import type { DataModel } from '@mcschema/core'
import type { Inputs } from 'preact/hooks'
import { useEffect } from 'preact/hooks'
export function useModel(model: DataModel | undefined | null, invalidated: (model: DataModel) => unknown, inputs?: Inputs) {
const listener = {
invalidated() {
if (model) {
invalidated(model)
}
},
}
useEffect(() => {
model?.addListener(listener)
return () => {
model?.removeListener(listener)
}
}, [model, ...inputs ?? []])
}

View File

@@ -1,11 +1,11 @@
import { useEffect, useErrorBoundary, useMemo } from 'preact/hooks'
import config from '../Config.js'
import { CustomizedPanel } from '../components/customized/CustomizedPanel.jsx'
import { ErrorPanel, Footer, Octicon, VersionSwitcher } from '../components/index.js'
import config from '../Config.js'
import { useLocale, useTitle, useVersion } from '../contexts/index.js'
import { useSearchParam } from '../hooks/index.js'
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'
const MIN_VERSION = '1.20'
const Tabs = ['basic', 'biomes', 'structures', 'ores']

View File

@@ -1,210 +0,0 @@
import type { CollectionRegistry, ResourceType, SchemaRegistry } from '@mcschema/core'
import { BooleanNode, Case, ChoiceNode, ListNode, MapNode, NumberNode, ObjectNode, Opt, Reference as RawReference, StringNode as RawStringNode, Switch } from '@mcschema/core'
const ID = 'immersive_weathering'
export function initImmersiveWeathering(schemas: SchemaRegistry, collections: CollectionRegistry) {
const Reference = RawReference.bind(undefined, schemas)
const StringNode = RawStringNode.bind(undefined, collections)
const Tag = (id: Exclude<ResourceType, `$tag/${string}`>) => ChoiceNode([
{
type: 'string',
node: StringNode({ validator: 'resource', params: { pool: id, allowTag: true } }),
change: (v: unknown) => {
if (Array.isArray(v) && typeof v[0] === 'string' && !v[0].startsWith('#')) {
return v[0]
}
return undefined
},
},
{
type: 'list',
node: ListNode(
StringNode({ validator: 'resource', params: { pool: id } })
),
change: (v: unknown) => {
if (typeof v === 'string' && !v.startsWith('#')) {
return [v]
}
return []
},
},
], { choiceContext: 'tag' })
schemas.register(`${ID}:block_growth`, ObjectNode({
area_condition: Reference(`${ID}:area_condition`),
position_predicates: Opt(ListNode(
Reference(`${ID}:position_test`)
)),
growth_chance: NumberNode({ min: 0, max: 1 }),
growth_for_face: ListNode(
ObjectNode({
direction: Opt(StringNode({ enum: 'direction' })),
weight: Opt(NumberNode({ integer: true })),
growth: ListNode(
ObjectNode({
data: Reference(`${ID}:block_pair`),
weight: NumberNode({ integer: true }),
})
),
}, { category: 'pool' })
),
owners: ListNode(
StringNode({ validator: 'resource', params: { pool: 'block' } })
),
replacing_target: Reference(`${ID}:rule_test`),
target_self: Opt(BooleanNode()),
destroy_target: Opt(BooleanNode()),
}, { context: `${ID}.block_growth` }))
schemas.register(`${ID}:area_condition`, ObjectNode({
type: StringNode({ enum: ['generate_if_not_too_many', 'neighbor_based_generation'] }),
[Switch]: [{ push: 'type' }],
[Case]: {
generate_if_not_too_many: {
radiusX: NumberNode({ integer: true }),
radiusY: NumberNode({ integer: true }),
radiusZ: NumberNode({ integer: true }),
requiredAmount: NumberNode({ integer: true }),
yOffset: Opt(NumberNode({ integer: true })),
must_have: Opt(Reference(`${ID}:rule_test`)),
must_not_have: Opt(Reference(`${ID}:rule_test`)),
includes: Opt(Tag('block')),
},
neighbor_based_generation: {
must_have: Reference(`${ID}:rule_test`),
must_not_have: Opt(Reference(`${ID}:rule_test`)),
required_amount: Opt(NumberNode({ integer: true })),
directions: ListNode(
StringNode({ enum: 'direction' })
),
},
},
}, { context: `${ID}.area_condition` }))
schemas.register(`${ID}:block_pair`, ObjectNode({
block: Reference(`${ID}:block_state`),
above_block: Opt(Reference(`${ID}:block_state`)),
}, { context: `${ID}.block_pair` }))
schemas.register(`${ID}:block_state`, ObjectNode({
Name: StringNode({ validator: 'resource', params: { pool: 'block' } }),
Properties: Opt(MapNode(
StringNode(),
StringNode(),
)),
}, { context: 'block_state' }))
schemas.register(`${ID}:position_test`, ObjectNode({
predicate_type: StringNode({ enum: ['biome_match', 'day_test', 'nand', 'precipitation_test', 'temperature_range'] }),
[Switch]: [{ push: 'predicate_type' }],
[Case]: {
biome_match: {
biomes: Tag('$worldgen/biome'),
},
day_test: {
day: BooleanNode(),
},
nand: {
predicates: ListNode(
Reference(`${ID}:position_test`)
),
},
precipitation_test: {
precipitation: StringNode({ enum: ['none', 'rain', 'snow']}),
},
temperature_range: {
min: NumberNode(),
max: NumberNode(),
use_local_pos: Opt(BooleanNode()),
},
},
}, { context: `${ID}.position_test`, category: 'predicate' }))
collections.register(`${ID}:rule_test`, [
...collections.get('rule_test'),
'immersive_weathering:block_set_match',
'immersive_weathering:fluid_match',
'immersive_weathering:tree_log',
])
schemas.register(`${ID}:rule_test`, ObjectNode({
predicate_type: StringNode({ validator: 'resource', params: { pool: `${ID}:rule_test` as any } }),
[Switch]: [{ push: 'predicate_type' }],
[Case]: {
'minecraft:block_match': {
block: StringNode({ validator: 'resource', params: { pool: 'block' } }),
},
'minecraft:blockstate_match': {
block_state: Reference('block_state'),
},
'minecraft:random_block_match': {
block: StringNode({ validator: 'resource', params: { pool: 'block' } }),
probability: NumberNode({ min: 0, max: 1 }),
},
'minecraft:random_blockstate_match': {
block_state: Reference('block_state'),
probability: NumberNode({ min: 0, max: 1 }),
},
'minecraft:tag_match': {
tag: StringNode({ validator: 'resource', params: { pool: '$tag/block' }}),
},
'immersive_weathering:block_set_match': {
blocks: Tag('block'),
probability: Opt(NumberNode({ min: 0, max: 1 })),
},
'immersive_weathering:fluid_match': {
fluid: StringNode({ validator: 'resource', params: { pool: 'fluid' } }),
},
},
}, { context: 'rule_test', disableSwitchContext: true }))
collections.register('block_growth', [
'immersive_weathering:brain_coral',
'immersive_weathering:bubble_coral',
'immersive_weathering:cracked_mud_rivers',
'immersive_weathering:crimson_nylium',
'immersive_weathering:cryosol',
'immersive_weathering:farmland_rare_weeds',
'immersive_weathering:farmland_weeds',
'immersive_weathering:fire_coral',
'immersive_weathering:fire_soot',
'immersive_weathering:fluvisol',
'immersive_weathering:grass_base',
'immersive_weathering:grass_block_badlands',
'immersive_weathering:grass_block_bamboo_jungle',
'immersive_weathering:grass_block_birch_forest',
'immersive_weathering:grass_block_dark_forest',
'immersive_weathering:grass_block_flower_forest',
'immersive_weathering:grass_block_forest',
'immersive_weathering:grass_block_jungle',
'immersive_weathering:grass_block_lush_caves',
'immersive_weathering:grass_block_old_growth_spruce',
'immersive_weathering:grass_block_plains',
'immersive_weathering:grass_block_sunflower_plains',
'immersive_weathering:grass_block_swamp',
'immersive_weathering:grass_block_taiga',
'immersive_weathering:grass_block_wooded_badlands',
'immersive_weathering:hanging_roots',
'immersive_weathering:horn_coral',
'immersive_weathering:humus',
'immersive_weathering:icicle_growth',
'immersive_weathering:large_fern',
'immersive_weathering:magma',
'immersive_weathering:mycelium',
'immersive_weathering:podzol',
'immersive_weathering:red_sand_weathering',
'immersive_weathering:rooted_dirt',
'immersive_weathering:rooted_grass',
'immersive_weathering:sand_weathering',
'immersive_weathering:sapling',
'immersive_weathering:sapling_nether',
'immersive_weathering:silt',
'immersive_weathering:tall_grass',
'immersive_weathering:tall_seagrass',
'immersive_weathering:tube_coral',
'immersive_weathering:vertisol',
'immersive_weathering:warped_nylium',
])
}

View File

@@ -1,239 +0,0 @@
import type { CollectionRegistry, ResourceType, SchemaRegistry } from '@mcschema/core'
import { BooleanNode, Case, ChoiceNode, ListNode, Mod, NumberNode, ObjectNode, Opt, Reference as RawReference, StringNode as RawStringNode, Switch } from '@mcschema/core'
import type { VersionId } from '../services/Schemas.js'
const ID = 'lithostitched'
export function initLithostitched(schemas: SchemaRegistry, collections: CollectionRegistry, version: VersionId) {
const Reference = RawReference.bind(undefined, schemas)
const StringNode = RawStringNode.bind(undefined, collections)
const Tag = (id: Exclude<ResourceType, `$tag/${string}`>) =>
ChoiceNode(
[
{
type: 'string',
node: StringNode({
validator: 'resource',
params: { pool: id, allowTag: true },
}),
change: (v: unknown) => {
if (
Array.isArray(v) &&
typeof v[0] === 'string' &&
!v[0].startsWith('#')
) {
return v[0]
}
return undefined
},
},
{
type: 'list',
node: ListNode(
StringNode({ validator: 'resource', params: { pool: id } })
),
change: (v: unknown) => {
if (typeof v === 'string' && !v.startsWith('#')) {
return [v]
}
return []
},
},
],
{ choiceContext: 'tag' }
)
// Worldgen Modifiers
const MobCategorySpawnSettings = Mod(
ObjectNode({
type: StringNode({
validator: 'resource',
params: { pool: 'entity_type' },
}),
weight: NumberNode({ integer: true }),
minCount: NumberNode({ integer: true }),
maxCount: NumberNode({ integer: true }),
}),
{
category: () => 'pool',
default: () => [
{
type: 'minecraft:bat',
weight: 1,
},
],
}
)
collections.register(`${ID}:modifier_type`, [
'lithostitched:add_biome_spawns',
'lithostitched:add_features',
...(version === '1.20.5' || version === '1.21')
? ['lithostitched:add_pool_aliases']
: [],
'lithostitched:add_structure_set_entries',
'lithostitched:add_surface_rule',
'lithostitched:add_template_pool_elements',
'lithostitched:no_op',
'lithostitched:redirect_feature',
'lithostitched:remove_biome_spawns',
'lithostitched:remove_features',
'lithostitched:remove_structures_from_structure_set',
'lithostitched:replace_climate',
'lithostitched:replace_effects',
])
collections.register(`${ID}:modifier_predicate_type`, [
'lithostitched:all_of',
'lithostitched:any_of',
'lithostitched:mod_loaded',
'lithostitched:not',
'lithostitched:true',
])
schemas.register(`${ID}:worldgen_modifier`, Mod(ObjectNode({
type: StringNode({ validator: 'resource', params: { pool: `${ID}:modifier_type` as any } }),
predicate: Mod(Opt(Reference(`${ID}:modifier_predicate`)), {
enabled: () => version !== '1.21',
}),
[Switch]: [{ push: 'type' }],
[Case]: {
'lithostitched:add_biome_spawns': {
biomes: Tag('$worldgen/biome'),
spawners: ChoiceNode([
{
type: 'object',
node: MobCategorySpawnSettings,
change: (v: any) => v[0],
},
{
type: 'list',
node: ListNode(MobCategorySpawnSettings),
change: (v: any) => Array(v),
},
]),
},
'lithostitched:add_features': {
biomes: Tag('$worldgen/biome'),
features: Tag('$worldgen/configured_feature'),
step: StringNode({ enum: 'decoration_step' }),
},
'lithostitched:add_pool_aliases': {
structure: StringNode({ validator: 'resource', params: { pool: '$worldgen/structure' } }),
pool_aliases: Reference('pool_alias_binding'),
},
'lithostitched:add_structure_set_entries': {
structure_set: StringNode({ validator: 'resource', params: { pool: '$worldgen/structure_set' } }),
entries: ListNode(
ObjectNode({
structure: StringNode({ validator: 'resource', params: { pool: '$worldgen/structure' } }),
weight: NumberNode({ integer: true, min: 1 }),
})
),
},
'lithostitched:add_surface_rule': {
levels: ListNode(StringNode({ validator: 'resource', params: { pool: '$dimension' } })),
surface_rule: Reference('material_rule'),
},
'lithostitched:add_template_pool_elements': {
template_pool: StringNode({ validator: 'resource', params: { pool: '$worldgen/template_pool' } }),
elements: ListNode(
Reference('template_weighted_element')
),
},
'lithostitched:redirect_feature': {
placed_feature: StringNode({ validator: 'resource', params: { pool: '$worldgen/placed_feature' } }),
redirect_to: StringNode({ validator: 'resource', params: { pool: '$worldgen/configured_feature' } }),
},
'lithostitched:remove_biome_spawns': {
biomes: Tag('$worldgen/biome'),
mobs: Tag('entity_type'),
},
'lithostitched:remove_features': {
biomes: Tag('$worldgen/biome'),
features: Tag('$worldgen/configured_feature'),
step: StringNode({ enum: 'decoration_step' }),
},
'lithostitched:remove_structures_from_structure_set': {
structure_set: StringNode({ validator: 'resource', params: { pool: '$worldgen/structure_set' } }),
structures: ListNode(
StringNode({ validator: 'resource', params: { pool: '$worldgen/structure' } })
),
},
'lithostitched:replace_climate': {
biomes: Tag('$worldgen/biome'),
climate: ObjectNode({
temperature: NumberNode(),
downfall: NumberNode(),
has_precipitation: BooleanNode(),
temperature_modifier: Opt(StringNode({ enum: ['none', 'frozen'] })),
}),
},
'lithostitched:replace_effects': {
biomes: Tag('$worldgen/biome'),
effects: ObjectNode({
sky_color: Opt(NumberNode({ color: true })),
fog_color: Opt(NumberNode({ color: true })),
water_color: Opt(NumberNode({ color: true })),
water_fog_color: Opt(NumberNode({ color: true })),
grass_color: Opt(NumberNode({ color: true })),
foliage_color: Opt(NumberNode({ color: true })),
grass_color_modifier: Opt(StringNode({ enum: ['none', 'dark_forest', 'swamp'] })),
ambient_sound: Opt(StringNode()),
mood_sound: Opt(ObjectNode({
sound: StringNode(),
tick_delay: NumberNode({ integer: true }),
block_search_extent: NumberNode({ integer: true }),
offset: NumberNode(),
})),
additions_sound: Opt(ObjectNode({
sound: StringNode(),
tick_chance: NumberNode({ min: 0, max: 1 }),
})),
music: Opt(ObjectNode({
sound: StringNode(),
min_delay: NumberNode({ integer: true, min: 0 }),
max_delay: NumberNode({ integer: true, min: 0 }),
replace_current_music: BooleanNode(),
})),
particle: Opt(ObjectNode({
options: ObjectNode({
type: StringNode(),
}),
probability: NumberNode({ min: 0, max: 1 }),
})),
}),
},
},
}, { context: `${ID}.worldgen_modifier`, disableSwitchContext: true }), {
default: () => ({
type: `${ID}:add_features`,
biomes: '#minecraft:is_overworld',
features: 'example:ore_ruby',
step: 'underground_ores',
}),
}))
schemas.register(`${ID}:modifier_predicate`, ObjectNode({
type: StringNode({ validator: 'resource', params: { pool: `${ID}:modifier_predicate_type` as any } }),
[Switch]: [{ push: 'type' }],
[Case]: {
'lithostitched:all_of': {
predicates: ListNode(Reference(`${ID}:modifier_predicate`)),
},
'lithostitched:any_of': {
predicates: ListNode(Reference(`${ID}:modifier_predicate`)),
},
'lithostitched:mod_loaded': {
mod_id: StringNode(),
},
'lithostitched:not': {
predicate: Reference(`${ID}:modifier_predicate`),
},
},
}, {
context: `${ID}.modifier_predicate`, disableSwitchContext: true,
}))
}

View File

@@ -1,492 +0,0 @@
import type { CollectionRegistry, INode, ResourceType, SchemaRegistry } from '@mcschema/core'
import { BooleanNode, Case, ChoiceNode, ListNode, MapNode, Mod, NumberNode, ObjectNode, Opt, StringNode as RawStringNode, Switch } from '@mcschema/core'
import type { VersionId } from '../services/Schemas.js'
const ID = 'neoforge'
export function initNeoForge(schemas: SchemaRegistry, collections: CollectionRegistry, _version: VersionId) {
const StringNode = RawStringNode.bind(undefined, collections)
// Homogenous list (ref, list of refs, tag)
const Tag = (id: Exclude<ResourceType, `$tag/${string}`>) =>
ChoiceNode(
[
{
type: 'string',
node: StringNode({
validator: 'resource',
params: { pool: id, allowTag: true },
}),
change: (v: unknown) => {
if (
Array.isArray(v) &&
typeof v[0] === 'string' &&
!v[0].startsWith('#')
) {
return v[0]
}
return undefined
},
},
{
type: 'list',
node: ListNode(
StringNode({ validator: 'resource', params: { pool: id } })
),
change: (v: unknown) => {
if (typeof v === 'string' && !v.startsWith('#')) {
return [v]
}
return []
},
},
],
{ choiceContext: 'tag' }
)
// Spawner data
const MobCategorySpawnSettings = Mod(
ObjectNode({
type: StringNode({ validator: 'resource', params: { pool: 'entity_type' }}),
weight: NumberNode({ integer: true, min: 0 }),
minCount: NumberNode({ integer: true, min: 1 }),
maxCount: NumberNode({ integer: true, min: 1 }),
}),
{
category: () => 'pool',
default: () => [
{
type: 'minecraft:bat',
weight: 1,
},
],
}
)
// Generation step carving
const CarvingStep = StringNode({ enum: [ 'air', 'liquid' ] })
// Mob category
const MobCategory = StringNode({ enum: [ 'monster', 'creature', 'ambient', 'axolotls', 'underground_water_creature', 'water_creature', 'water_ambient', 'misc' ], additional: true })
// Biome modifier types
collections.register(`${ID}:biome_modifier_type`, [
'neoforge:none',
'neoforge:add_features',
'neoforge:remove_features',
'neoforge:add_spawns',
'neoforge:remove_spawns',
'neoforge:add_carvers',
'neoforge:remove_carvers',
'neoforge:add_spawn_costs',
'neoforge:remove_spawn_costs',
])
// Biome modifiers
schemas.register(`${ID}:biome_modifier`, Mod(
ObjectNode({
type: StringNode({ validator: 'resource', params: { pool: `${ID}:biome_modifier_type` as any }}),
[Switch]: [{ push: 'type' }],
[Case]: {
'neoforge:none': {},
'neoforge:add_features': {
biomes: Tag('$worldgen/biome'),
features: Tag('$worldgen/placed_feature'),
step: StringNode({ enum: 'decoration_step' }),
},
'neoforge:remove_features': {
biomes: Tag('$worldgen/biome'),
features: Tag('$worldgen/placed_feature'),
steps: Opt(ChoiceNode([
{
type: 'string',
node: StringNode({ enum: 'decoration_step' }),
change: (v: any) => v[0],
},
{
type: 'list',
node: ListNode(StringNode({ enum: 'decoration_step' })),
change: (v: any) => Array(v),
},
])),
},
'neoforge:add_spawns': {
biomes: Tag('$worldgen/biome'),
spawners: ChoiceNode([
{
type: 'object',
node: MobCategorySpawnSettings,
change: (v: any) => v[0],
},
{
type: 'list',
node: MobCategorySpawnSettings,
change: (v: any) => Array(v),
},
]),
},
'neoforge:remove_spawns': {
biomes: Tag('$worldgen/biome'),
entity_types: Tag('entity_type'),
},
'neoforge:add_carvers': {
biomes: Tag('$worldgen/biome'),
carvers: Tag('$worldgen/configured_carver'),
step: CarvingStep,
},
'neoforge:remove_carvers': {
biomes: Tag('$worldgen/biome'),
carvers: Tag('$worldgen/configured_carver'),
steps: Opt(ChoiceNode([
{
type: 'string',
node: CarvingStep,
change: (v: any) => v[0],
},
{
type: 'list',
node: ListNode(CarvingStep),
change: (v: any) => Array(v),
},
])),
},
'neoforge:add_spawn_costs': {
biomes: Tag('$worldgen/biome'),
entity_types: Tag('entity_type'),
spawn_cost: ObjectNode({
energy_budget: NumberNode(),
charge: NumberNode(),
}),
},
'neoforge:remove_spawn_costs': {
biomes: Tag('$worldgen/biome'),
entity_types: Tag('entity_type'),
},
},
}, { context: `${ID}.biome_modifier`, disableSwitchContext: true }),
{
default: () => ({
type: `${ID}:add_features`,
biomes: '#minecraft:is_overworld',
features: 'minecraft:ore_iron_small',
step: 'underground_ores',
}),
}
))
// Structure modifier types
collections.register(`${ID}:structure_modifier_type`, [
'neoforge:none',
'neoforge:add_spawns',
'neoforge:remove_spawns',
'neoforge:clear_spawns',
])
// Structure modifiers
schemas.register(`${ID}:structure_modifier`, Mod(
ObjectNode({
type: StringNode({ validator: 'resource', params: { pool: `${ID}:structure_modifier_type` as any }}),
[Switch]: [{ push: 'type' }],
[Case]: {
'neoforge:none': {},
'neoforge:add_spawns': {
structures: Tag('$worldgen/structure'),
spawners: ChoiceNode([
{
type: 'object',
node: MobCategorySpawnSettings,
change: (v: any) => v[0],
},
{
type: 'list',
node: MobCategorySpawnSettings,
change: (v: any) => Array(v),
},
]),
},
'neoforge:remove_spawns': {
structures: Tag('$worldgen/structure'),
entity_types: Tag('entity_type'),
},
'neoforge:clear_spawns': {
structures: Tag('$worldgen/structure'),
categories: ChoiceNode([
{
type: 'string',
node: MobCategory,
change: (v: any) => v[0],
},
{
type: 'list',
node: MobCategory,
change: (v: any) => Array(v),
},
]),
},
},
}, { context: `${ID}.structure_modifier`, disableSwitchContext: true }),
{
default: () => ({
type: `${ID}:add_spawns`,
structures: '#minecraft:village',
spawners: {
type: 'minecraft:bat',
weight: 1,
},
}),
}
))
// Data maps
createDataMap(schemas, collections, 'compostables', 'item', ChoiceNode([
{
type: 'number',
node: NumberNode({
min: 0,
max: 1,
}),
change: (v: any) => v?.chance,
},
{
type: 'object',
node: ObjectNode({
chance: NumberNode({
min: 0,
max: 1,
}),
can_villager_compost: Opt(BooleanNode()),
}),
change: (v: any) => ({
chance: v,
can_villager_compost: false,
}),
},
]), (values) => values['minecraft:apple'] = {
chance: 1,
can_villager_compost: true,
}
)
createDataMap(schemas, collections, 'furnace_fuels', 'item', ChoiceNode([
{
type: 'number',
node: NumberNode({
min: 1,
integer: true,
}),
change: (v: any) => v?.burn_time,
},
{
type: 'object',
node: ObjectNode({
burn_time: NumberNode({
min: 1,
integer: true,
}),
}),
change: (v: any) => ({
burn_time: v,
}),
},
]), (values) => values['minecraft:chest'] = {
burn_time: 300,
}
)
createDataMap(schemas, collections, 'monster_room_mobs', 'entity_type', ChoiceNode([
{
type: 'number',
node: NumberNode({
min: 0,
integer: true,
}),
change: (v: any) => v?.weight,
},
{
type: 'object',
node: ObjectNode({
weight: NumberNode({
min: 0,
integer: true,
}),
}),
change: (v: any) => ({
weight: v,
}),
},
]), (values) => values['minecraft:bat'] = {
weight: 5,
})
createDataMap(schemas, collections, 'oxidizables', 'block', ChoiceNode([
{
type: 'string',
node: StringNode({
validator: 'resource',
params: { pool: 'block' },
}),
change: (v: any) => v?.next_oxidation_stage,
},
{
type: 'object',
node: ObjectNode({
next_oxidation_stage: StringNode({
validator: 'resource',
params: { pool: 'block' },
}),
}),
change: (v: any) => ({
next_oxidation_stage: v,
}),
},
]), (values) => values['minecraft:grass_block'] = {
next_oxidation_stage: 'minecraft:dirt',
})
createDataMap(schemas, collections, 'parrot_imitations', 'entity_type', ChoiceNode([
{
type: 'string',
node: StringNode({
validator: 'resource',
params: { pool: 'sound_event' as any },
}),
change: (v: any) => v?.sound,
},
{
type: 'object',
node: ObjectNode({
sound: StringNode({
validator: 'resource',
params: { pool: 'sound_event' as any },
}),
}),
change: (v: any) => ({
sound: v,
}),
},
]), (values) => values['minecraft:allay'] = {
sound: 'minecraft:entity.allay.ambient_without_item',
})
createDataMap(schemas, collections, 'raid_hero_gifts', 'villager_profession', ChoiceNode([
{
type: 'string',
node: StringNode({
validator: 'resource',
params: { pool: '$loot_table' },
}),
change: (v: any) => v?.loot_table,
},
{
type: 'object',
node: ObjectNode({
loot_table: StringNode({
validator: 'resource',
params: { pool: '$loot_table' },
}),
}),
change: (v: any) => ({
loot_table: v,
}),
},
]), (values) => values['minecraft:cleric'] = {
loot_table: 'minecraft:empty',
})
createDataMap(schemas, collections, 'vibration_frequencies', 'game_event', ChoiceNode([
{
type: 'number',
node: NumberNode({
min: 1,
max: 15,
integer: true,
}),
change: (v: any) => v?.frequency,
},
{
type: 'object',
node: ObjectNode({
frequency: NumberNode({
min: 1,
max: 15,
integer: true,
}),
}),
change: (v: any) => ({
frequency: v,
}),
},
]), (values) => values['minecraft:block_change'] = {
frequency: 5,
})
createDataMap(schemas, collections, 'waxables', 'block', ChoiceNode([
{
type: 'string',
node: StringNode({
validator: 'resource',
params: { pool: 'block' },
}),
change: (v: any) => v?.waxed,
},
{
type: 'object',
node: ObjectNode({
waxed: StringNode({
validator: 'resource',
params: { pool: 'block' },
}),
}),
change: (v: any) => ({
waxed: v,
}),
},
]), (values) => values['minecraft:dirt'] = {
waxed: 'minecraft:coarse_dirt',
})
}
function createDataMap(schemas: SchemaRegistry, collections: CollectionRegistry, dataMap: string, registry: ResourceType, valueNode: INode<any>, def: (values: any) => void) {
const StringNode = RawStringNode.bind(undefined, collections)
// Ref or tag
const Tag = StringNode({
validator: 'resource',
params: { pool: registry, allowTag: true },
})
// Create data map
schemas.register(`${ID}:data_map_${dataMap}`, Mod(
ObjectNode({
replace: Opt(BooleanNode()),
values: MapNode(
Tag,
ChoiceNode([
{
type: 'direct',
match: () => true,
node: valueNode,
change: (v: any) => v?.value,
},
{
type: 'replaceable',
match: (v: any) => typeof v === 'object' && v?.value !== undefined,
priority: 1,
node: ObjectNode({
replace: Opt(BooleanNode()),
value: valueNode,
}),
change: (v: any) => ({
replace: true,
value: v,
}),
},
]),
),
remove: Opt(ListNode(Tag)),
}, {context: `${ID}.data_map_${dataMap}`, disableSwitchContext: true}),
{
default: () => {
const result = {
values: {},
}
def(result.values)
return result
},
}
))
}

View File

@@ -1,442 +0,0 @@
import type { CollectionRegistry, SchemaRegistry } from '@mcschema/core'
import { BooleanNode, Case, ListNode, MapNode, Mod, NumberNode, ObjectNode, Opt, Reference as RawReference, StringNode as RawStringNode, Switch } from '@mcschema/core'
const ID = 'obsidian'
export function initObsidian(schemas: SchemaRegistry, collections: CollectionRegistry) {
const Reference = RawReference.bind(undefined, schemas)
const StringNode = RawStringNode.bind(undefined, collections)
// ITEMS
schemas.register(`${ID}:item`, Mod(ObjectNode({
information: Opt(Reference(`${ID}:item_information`)),
display: Opt(ObjectNode({
model: Opt(Reference(`${ID}:model`)),
item_model: Opt(Reference(`${ID}:model`)),
lore: Opt(ListNode(
ObjectNode({
text: Reference(`${ID}:name_information`),
}),
)),
})),
use_action: Opt(ObjectNode({
action: Opt(StringNode({ enum: ['none', 'eat', 'drink', 'block', 'bow', 'spear', 'crossbow', 'spyglass'] })),
right_click_action: Opt(StringNode({ enum: ['open_gui', 'run_command', 'open_url']})) ,
command: Opt(StringNode()),
url: Opt(StringNode()),
gui_size: Opt(NumberNode({ integer: true, min: 1, max: 6 })),
gui_title: Opt(Reference(`${ID}:name_information`)),
})),
}, { context: `${ID}:item` }), {
default: () => ({}),
}))
schemas.register(`${ID}:item_information`, ObjectNode({
rarity: Opt(StringNode({ enum: ['common', 'uncommon', 'rare', 'epic']})),
creative_tab: Opt(StringNode()),
max_stack_size: Opt(NumberNode({ integer: true, min: 1 })),
name: Opt(Reference(`${ID}:name_information`)),
has_enchantment_glint: Opt(BooleanNode()),
is_enchantable: Opt(BooleanNode()),
enchantability: Opt(NumberNode({ integer: true })),
use_duration: Opt(NumberNode({ integer: true })),
can_place_block: Opt(BooleanNode()),
placable_block: Opt(StringNode({ validator: 'resource', params: { pool: 'block' } })),
wearable: Opt(BooleanNode()),
default_color: Opt(NumberNode({ color: true })),
wearable_slot: Opt(StringNode()),
custom_render_mode: Opt(BooleanNode()),
render_mode_models: Opt(ListNode(
ObjectNode({
model: Reference('model_identifier'),
modes: ListNode(StringNode()),
})
)),
}, { context: `${ID}:item_information` }))
schemas.register(`${ID}:item_model`, ObjectNode({
textures: Opt(MapNode(
StringNode(),
StringNode({ validator: 'resource', params: { pool: '$texture' } }),
)),
parent: StringNode({ validator: 'resource', params: { pool: '$model'} }),
}, { context: `${ID}:item_model` }))
schemas.register(`${ID}:block`, Mod(ObjectNode({
block_type: Opt(StringNode({ enum: `${ID}:block_type`})),
information: Opt(Reference(`${ID}:block_information`)),
display: Opt(ObjectNode({
model: Opt(Reference(`${ID}:model`)),
item_model: Opt(Reference(`${ID}:model`)),
block_model: Opt(Reference(`${ID}:model`)),
lore: ListNode(
ObjectNode({
text: Reference(`${ID}:item_name_information`),
}),
),
})),
additional_information: Opt(ObjectNode({
extraBlocksName: Opt(StringNode()),
slab: Opt(BooleanNode()),
stairs: Opt(BooleanNode()),
walls: Opt(BooleanNode()),
fence: Opt(BooleanNode()),
fenceGate: Opt(BooleanNode()),
button: Opt(BooleanNode()),
pressurePlate: Opt(BooleanNode()),
door: Opt(BooleanNode()),
trapdoor: Opt(BooleanNode()),
path: Opt(BooleanNode()),
lantern: Opt(BooleanNode()),
barrel: Opt(BooleanNode()),
leaves: Opt(BooleanNode()),
plant: Opt(BooleanNode()),
chains: Opt(BooleanNode()),
cake_like: Opt(BooleanNode()),
waterloggable: Opt(BooleanNode()),
dyable: Opt(BooleanNode()),
defaultColor: Opt(NumberNode({ color: true })),
sittable: Opt(BooleanNode()),
isConvertible: Opt(BooleanNode()),
convertible: Opt(ObjectNode({
drops_item: Opt(BooleanNode()),
reversible: Opt(BooleanNode()),
parent_block: Opt(StringNode({ validator: 'resource', params: { pool: 'block' } })),
transformed_block: Opt(StringNode({ validator: 'resource', params: { pool: 'block' } })),
dropped_item: Opt(StringNode({ validator: 'resource', params: { pool: 'item' } })),
sound: Opt(StringNode()),
conversionItem: Opt(ObjectNode({
item: StringNode({ validator: 'resource', params: { pool: 'item' } }),
tag: StringNode({ validator: 'resource', params: { pool: '$tag/item' } }),
})),
reversalItem: Opt(ObjectNode({
item: StringNode({ validator: 'resource', params: { pool: 'item' } }),
tag: StringNode({ validator: 'resource', params: { pool: '$tag/item' } }),
})),
})),
})),
functions: Opt(ObjectNode({
random_tick: Opt(Reference(`${ID}:function`)),
scheduled_tick: Opt(Reference(`${ID}:function`)),
random_display_tick: Opt(Reference(`${ID}:function`)),
place: Opt(Reference(`${ID}:function`)),
break: Opt(Reference(`${ID}:function`)),
use: Opt(Reference(`${ID}:function`)),
walk_on: Opt(Reference(`${ID}:function`)),
})),
ore_information: Opt(ObjectNode({
test_type: Opt(StringNode({ enum: ['tag', 'always', 'block_match', 'block_state_match', 'random_block_match', 'random_block_state_match']})),
target_state: Opt(ObjectNode({
block: Opt(StringNode({ validator: 'resource', params: { pool: 'block' }})),
tag: Opt(StringNode({ validator: 'resource', params: { pool: '$tag/block' } })),
properties: Opt(MapNode(
StringNode(),
StringNode(),
)),
probability: Opt(NumberNode({ min: 0, max: 1 })),
})),
triangleRange: Opt(BooleanNode()),
plateau: Opt(NumberNode({ integer: true, min: 0 })),
spawnPredicate: Opt(StringNode({ enum: ['built_in', 'vanilla', 'overworld', 'the_nether', 'the_end', 'categories', 'biomes'] })),
biomeCategories: ListNode(StringNode()),
biomes: ListNode(
StringNode({ validator: 'resource', params: { pool: '$worldgen/biome' }})
),
size: Opt(NumberNode({ integer: true })),
chance: Opt(NumberNode({ integer: true, min: 1 })),
discardOnAirChance: Opt(NumberNode({ min: 0, max: 1 })),
topOffset: Reference(`${ID}:block_y_offset`),
bottomOffset: Reference(`${ID}:block_y_offset`),
})),
food_information: Opt(ObjectNode({
hunger: Opt(NumberNode({ integer: true, min: 0 })),
saturation: Opt(NumberNode({ integer: true, min: 0 })),
effects: ListNode(
ObjectNode({
effect: StringNode({ validator: 'resource', params: { pool: 'mob_effect' } }),
duration: Opt(NumberNode({ integer: true, min: 0 })),
amplifier: Opt(NumberNode({ integer: true, min: 0 })),
chance: Opt(NumberNode({ integer: true, min: 0, max: 1 })),
})
),
})),
cake_slices: Opt(NumberNode({integer: true, min: 1})),
campfire_properties: Opt(ObjectNode({
emits_particles: Opt(BooleanNode()),
fire_damage: Opt(NumberNode({ integer: true })),
luminance: Opt(NumberNode({ integer: true })),
})),
particle_type: Opt(StringNode()),
can_plant_on: Opt(ListNode(
StringNode({ validator: 'resource', params: { pool: 'block' } })
)),
growable: Opt(ObjectNode({
min_age: Opt(NumberNode({ integer: true, min: 1 })),
max_age: Opt(NumberNode({ integer: true })),
})),
oxidizable_properties: Opt(ObjectNode({
stages: Opt(ListNode(
ObjectNode({
can_be_waxed: Opt(BooleanNode()),
stairs: Opt(BooleanNode()),
slab: Opt(BooleanNode()),
blocks: Opt(ListNode(
ObjectNode({
name: Opt(Reference(`${ID}:name_information`)),
display: Opt(Reference(`${ID}:model`)),
})
)),
})
)),
})),
events: Opt(ListNode(Reference(`${ID}:event`))),
drop_information: Opt(ObjectNode({
drops: Opt(ListNode(
ObjectNode({
name: StringNode({ validator: 'resource', params: { pool: 'item' } }),
drops_if_silk_touch: Opt(BooleanNode()),
})
)),
survives_explosion: Opt(BooleanNode()),
xp_drop_amount: Opt(NumberNode({ integer: true })),
})),
is_multi_block: Opt(BooleanNode()),
multiblock_information: Opt(ObjectNode({
width: Opt(NumberNode({ integer: true })),
height: Opt(NumberNode({ integer: true })),
})),
placable_feature: Opt(StringNode({ validator: 'resource', params: { pool: '$worldgen/configured_feature' } })),
}, { context: `${ID}:block` }), {
default: () => ({}),
}))
schemas.register(`${ID}:block_information`, ObjectNode({
rarity: Opt(StringNode({ enum: ['common', 'uncommon', 'rare', 'epic']})),
creative_tab: Opt(StringNode()),
collidable: Opt(BooleanNode()),
max_stack_size: Opt(NumberNode({ integer: true, min: 1 })),
name: Opt(Reference(`${ID}:name_information`)),
vanilla_sound_group: Opt(StringNode()),
custom_sound_group: Opt(Reference(`${ID}:sound_group`)),
vanilla_material: Opt(StringNode()),
custom_material: Opt(Reference(`${ID}:material`)),
has_glint: Opt(BooleanNode()),
is_enchantable: Opt(BooleanNode()),
enchantability: Opt(NumberNode({ integer: true })),
fireproof: Opt(BooleanNode()),
translucent: Opt(BooleanNode()),
dynamic_boundaries: Opt(BooleanNode()),
has_item: Opt(BooleanNode()),
dyeable: Opt(BooleanNode()),
defaultColor: Opt(NumberNode({ color: true })),
wearable: Opt(BooleanNode()),
wearble_slot: Opt(StringNode()),
custom_render_mode: Opt(BooleanNode()),
render_mode_models: Opt(ListNode(
ObjectNode({
model: Reference('model_identifier'),
modes: ListNode(StringNode()),
})
)),
}, { context: `${ID}:block_information` }))
schemas.register(`${ID}:block_y_offset`, ObjectNode({
type: Opt(StringNode({enum: ['fixed', 'above_bottom', 'below_top', 'bottom', 'top']})),
offset: Opt(NumberNode({ integer: true })),
}))
schemas.register(`${ID}:function`, ObjectNode({
predicate: Opt(Reference(`${ID}:predicate`)),
function_type: StringNode({ enum: ['NONE', 'REQUIRES_SHIFTING', 'REQUIRES_ITEM', 'REQUIRES_SHIFTING_AND_ITEM'] }),
[Switch]: [{ push: 'function_type' }],
[Case]: {
NONE: {},
REQUIRES_SHIFTING: {},
REQUIRES_ITEM: { item: StringNode({ validator: 'resource', params: { pool: 'item' } }) },
REQUIRES_SHIFTING_AND_ITEM: { item: StringNode({ validator: 'resource', params: { pool: 'item' } }) },
},
function_file: StringNode(),
}, { context: `${ID}:function` }))
schemas.register(`${ID}:predicate`, ObjectNode({
predicate_type: Opt(StringNode({ enum: ['ALWAYS', 'EQUALS', 'NOT_EQUALS', 'CONTAINS', 'NOT_CONTAINS', 'BEGINS_WITH', 'ENDS_WITH', 'REGEX'] })),
[Switch]: [{ push: 'predicate_type' }],
[Case]: {
ALWAYS: {},
EQUALS: { left: StringNode(), right: StringNode() },
NOT_EQUALS: { left: StringNode(), right: StringNode() },
CONTAINS: { left: StringNode(), right: StringNode() },
NOT_CONTAINS: { left: StringNode(), right: StringNode() },
BEGINS_WITH: { left: StringNode(), right: StringNode() },
ENDS_WITH: { left: StringNode(), right: StringNode() },
REGEX: { left: StringNode(), right: StringNode() },
},
}, { context: `${ID}:predicate` }))
schemas.register(`${ID}:model`, ObjectNode({
textures: Opt(MapNode(
StringNode(),
StringNode({ validator: 'resource', params: { pool: '$texture' } }),
)),
parent: StringNode({ validator: 'resource', params: { pool: '$model'} }),
}, { context: `${ID}:model` }))
schemas.register(`${ID}:sound_group`, ObjectNode({
id: Opt(StringNode()),
break_sound: Opt(StringNode()),
step_sound: Opt(StringNode()),
place_sound: Opt(StringNode()),
hit_sound: Opt(StringNode()),
}, { context: `${ID}:sound_group` }))
schemas.register(`${ID}:material`, ObjectNode({
id: Opt(StringNode()),
map_color: Opt(StringNode()),
allows_movement: Opt(BooleanNode()),
burnable: Opt(BooleanNode()),
liquid: Opt(BooleanNode()),
allows_light: Opt(BooleanNode()),
replacable: Opt(BooleanNode()),
solid: Opt(BooleanNode()),
piston_behaviour: Opt(StringNode({ enum: ['NORMAL', 'DESTROY', 'BLOCK', 'IGNORE', 'PUSH_ONLY'] })),
}, { context: `${ID}:material` }))
// COMMON
schemas.register(`${ID}:name_information`, ObjectNode({
id: StringNode(),
text: Opt(StringNode()),
type: Opt(StringNode({ enum: ['literal', 'translatable'] })),
translated: Opt(MapNode(
StringNode(),
StringNode(),
)),
color: Opt(StringNode()),
formatting: Opt(ListNode(StringNode())),
}))
schemas.register(`${ID}:display_information`, ObjectNode({
// TODO
}))
schemas.register(`${ID}:event`, ObjectNode({
activations: StringNode({enum: ['use', 'shift_use', 'collide', 'walk_on']}),
predicate: Opt(StringNode({ validator: 'resource', params: { pool: '$predicate' } })),
type: StringNode({ enum: ['give_effect', 'damage', 'decrement_stack', 'kill', 'play_sound', 'remove_effect', 'run_command', 'set_block', 'set_block_at_pos', 'set_block_property', 'spawn_loot', 'spawn_entity'] }),
[Switch]: [{ push: 'type' }],
[Case]: {
give_effect: {
amplifier: NumberNode({ integer: true }),
duration: NumberNode(),
effect: StringNode({ validator: 'resource', params: { pool: 'mob_effect' } }),
target: StringNode(),
},
damage: {
amount: NumberNode({ integer: true }),
target: StringNode(),
damage_type: StringNode(),
},
decrement_stack: {
amount: NumberNode({ integer: true }),
},
kill: {
target: StringNode(),
},
remove_effect: {
effect: StringNode({ validator: 'resource', params: { pool: 'mob_effect' } }),
target: StringNode(),
},
run_command: {
command: StringNode(),
target: StringNode(),
},
set_block: {
block: StringNode({ validator: 'resource', params: { pool: 'block' } }),
x_pos: NumberNode(),
y_pos: NumberNode(),
z_pos: NumberNode(),
},
set_block_at_pos: {
block: StringNode({ validator: 'resource', params: { pool: 'block' } }),
x_pos: NumberNode(),
y_pos: NumberNode(),
z_pos: NumberNode(),
},
set_block_property: {
block: StringNode({ validator: 'resource', params: { pool: 'block' } }),
property: StringNode(),
value: StringNode(),
},
spawn_loot: {
loot_table: StringNode({ validator: 'resource', params: { pool: '$loot_table' } }),
x_pos: NumberNode(),
y_pos: NumberNode(),
z_pos: NumberNode(),
},
spawn_entity: {
entity_type: StringNode({ validator: 'resource', params: { pool: 'entity_type' } }),
x_pos: NumberNode(),
y_pos: NumberNode(),
z_pos: NumberNode(),
amount: NumberNode({ integer: true }),
},
},
}))
// COLLECTIONS
collections.register(`${ID}:block_type`, [
'BLOCK',
'HORIZONTAL_FACING_BLOCK',
'ROTATABLE_BLOCK',
'CAMPFIRE',
'STAIRS',
'SLAB',
'WALL',
'FENCE',
'FENCE_GATE',
'CAKE',
'BED',
'TRAPDOOR',
'METAL_DOOR',
'WOODEN_DOOR',
'LOG',
'STEM',
'WOOD',
'OXIDIZING_BLOCK',
'PLANT',
'PILLAR',
'HORIZONTAL_FACING_PLANT',
'SAPLING',
'TORCH',
'BEEHIVE',
'LEAVES',
'LADDER',
'PATH',
'WOODEN_BUTTON',
'STONE_BUTTON',
'DOUBLE_PLANT',
'HORIZONTAL_FACING_DOUBLE_PLANT',
'HANGING_DOUBLE_LEAVES',
'EIGHT_DIRECTIONAL_BLOCK',
'LANTERN',
'CHAIN',
'PANE',
'DYEABLE',
'LOOM',
'GRINDSTONE',
'CRAFTING_TABLE',
'PISTON',
'NOTEBLOCK',
'JUKEBOX',
'SMOKER',
'FURNACE',
'BLAST_FURNACE',
'LECTERN',
'FLETCHING_TABLE',
'BARREL',
'COMPOSTER',
'RAILS',
'CARTOGRAPHY_TABLE',
'CARPET',
])
}

View File

@@ -1,114 +0,0 @@
import type { CollectionRegistry, SchemaRegistry } from '@mcschema/core'
import { Case, ListNode, Mod, NumberNode, ObjectNode, Opt, Reference as RawReference, StringNode as RawStringNode, Switch } from '@mcschema/core'
export function initOhTheTreesYoullGrow(schemas: SchemaRegistry, collections: CollectionRegistry) {
const Reference = RawReference.bind(undefined, schemas)
const StringNode = RawStringNode.bind(undefined, collections)
collections.register('ohthetreesyoullgrow:feature', [
'ohthetreesyoullgrow:tree_from_nbt_v1',
])
const BlockSet = ListNode(
StringNode({ validator: 'resource', params: { pool: 'block' } })
)
schemas.register('ohthetreesyoullgrow:configured_feature', Mod(ObjectNode({
type: StringNode({ validator: 'resource', params: { pool: 'ohthetreesyoullgrow:feature' as any } }),
config: ObjectNode({
[Switch]: ['pop', { push: 'type' }],
[Case]: {
'ohthetreesyoullgrow:tree_from_nbt_v1': {
base_location: StringNode({ validator: 'resource', params: { pool: '$structure' } }),
canopy_location: StringNode({ validator: 'resource', params: { pool: '$structure' } }),
can_grow_on_filter: Reference('block_predicate_worldgen'),
can_leaves_place_filter: Reference('block_predicate_worldgen'),
decorators: Opt(ListNode(
ObjectNode({
type: StringNode({ validator: 'resource', params: { pool: 'worldgen/tree_decorator_type' } }),
[Switch]: [{ push: 'type' }],
[Case]: {
'minecraft:alter_ground': {
provider: Reference('block_state_provider'),
},
'minecraft:attached_to_leaves': {
probability: NumberNode({ min: 0, max: 1 }),
exclusion_radius_xz: NumberNode({ integer: true, min: 0, max: 16 }),
exclusion_radius_y: NumberNode({ integer: true, min: 0, max: 16 }),
required_empty_blocks: NumberNode({ integer: true, min: 1, max: 16 }),
block_provider: Reference('block_state_provider'),
directions: ListNode(
StringNode({ enum: 'direction' })
),
},
'minecraft:beehive': {
probability: NumberNode({ min: 0, max: 1 }),
},
'minecraft:cocoa': {
probability: NumberNode({ min: 0, max: 1 }),
},
'minecraft:leave_vine': {
probability: NumberNode({ min: 0, max: 1 }),
},
},
}, { context: 'tree_decorator' })
)),
height: Reference('int_provider'),
leaves_provider: Reference('block_state_provider'),
leaves_target: BlockSet,
log_provider: Reference('block_state_provider'),
log_target: BlockSet,
max_log_depth: Opt(NumberNode({ integer: true })),
place_from_nbt: BlockSet,
},
},
}, { disableSwitchContext: true }),
}, { context: 'ohthetreesyoullgrow.configured_feature' }), {
default: () => ({
type: 'ohthetreesyoullgrow:tree_from_nbt_v1',
config: {
can_grow_on_filter: {
type: 'minecraft:matching_block_tag',
tag: 'minecraft:dirt',
},
can_leaves_place_filter: {
type: 'minecraft:replaceable',
},
height: {
type: 'minecraft:uniform',
value: {
min_inclusive: 5,
max_inclusive: 10,
},
},
leaves_provider: {
type: 'minecraft:simple_state_provider',
state: {
Name: 'minecraft:acacia_leaves',
Properties: {
distance: '7',
persistent: 'false',
waterlogged: 'false',
},
},
},
leaves_target: [
'minecraft:oak_leaves',
],
log_provider: {
type: 'minecraft:simple_state_provider',
state: {
Name: 'minecraft:acacia_log',
Properties: {
axis: 'y',
},
},
},
log_target: [
'minecraft:oak_log',
],
place_from_nbt: [],
},
}),
}))
}

View File

@@ -1,18 +0,0 @@
import type { CollectionRegistry, SchemaRegistry } from '@mcschema/core'
import type { VersionId } from '../services/Schemas.js'
import { initImmersiveWeathering } from './ImmersiveWeathering.js'
import { initLithostitched } from './Lithostitched.js'
import { initNeoForge } from './NeoForge.js'
import { initObsidian } from './Obsidian.js'
import { initOhTheTreesYoullGrow } from './OhTheTreesYoullGrow.js'
export * from './ImmersiveWeathering.js'
export * from './Lithostitched.js'
export function initPartners(schemas: SchemaRegistry, collections: CollectionRegistry, version: VersionId) {
initImmersiveWeathering(schemas, collections)
initLithostitched(schemas, collections, version)
initNeoForge(schemas, collections, version)
initObsidian(schemas, collections)
initOhTheTreesYoullGrow(schemas, collections)
}

View File

@@ -1,176 +0,0 @@
{
"immersive_weathering.area_condition.type": "Type",
"immersive_weathering.area_condition.type.generate_if_not_too_many": "Generate if not too many",
"immersive_weathering.area_condition.type.neighbor_based_generation": "Neighbor based generation",
"immersive_weathering.area_condition.generate_if_not_too_many.radiusX": "Radius X",
"immersive_weathering.area_condition.generate_if_not_too_many.radiusY": "Radius Y",
"immersive_weathering.area_condition.generate_if_not_too_many.radiusZ": "Radius Z",
"immersive_weathering.area_condition.generate_if_not_too_many.requiredAmount": "Required amount",
"immersive_weathering.area_condition.generate_if_not_too_many.yOffset": "Y offset",
"immersive_weathering.area_condition.generate_if_not_too_many.must_have": "Must have",
"immersive_weathering.area_condition.generate_if_not_too_many.must_not_have": "Must not have",
"immersive_weathering.area_condition.generate_if_not_too_many.includes": "Includes",
"immersive_weathering.area_condition.neighbor_based_generation.must_have": "Must have",
"immersive_weathering.area_condition.neighbor_based_generation.must_not_have": "Must not have",
"immersive_weathering.area_condition.neighbor_based_generation.required_amount": "Required amount",
"immersive_weathering.area_condition.neighbor_based_generation.directions": "Directions",
"immersive_weathering.area_condition.neighbor_based_generation.directions.entry": "Direction",
"immersive_weathering.block_growth.area_condition": "Area conditions",
"immersive_weathering.block_growth.position_predicates": "Position predicates",
"immersive_weathering.block_growth.position_predicates.entry": "Position test",
"immersive_weathering.block_growth.growth_chance": "Growth chance",
"immersive_weathering.block_growth.growth_for_face": "Growth for face",
"immersive_weathering.block_growth.growth_for_face.entry": "Face",
"immersive_weathering.block_growth.growth_for_face.entry.direction": "Direction",
"immersive_weathering.block_growth.growth_for_face.entry.weight": "Weight",
"immersive_weathering.block_growth.growth_for_face.entry.growth": "Growth",
"immersive_weathering.block_growth.growth_for_face.entry.growth.entry.data": "Block pair",
"immersive_weathering.block_growth.growth_for_face.entry.growth.entry.weight": "Weight",
"immersive_weathering.block_growth.owners": "Owners",
"immersive_weathering.block_growth.owners.entry": "Block",
"immersive_weathering.block_growth.replacing_target": "Replacing target",
"immersive_weathering.block_growth.target_self": "Target self",
"immersive_weathering.block_growth.destroy_target": "Destroy target",
"immersive_weathering.block_pair.block": "Block",
"immersive_weathering.block_pair.above_block": "Above block",
"immersive_weathering.position_test.predicate_type": "Predicate type",
"immersive_weathering.position_test.predicate_type.biome_match": "Biome match",
"immersive_weathering.position_test.predicate_type.day_test": "Day test",
"immersive_weathering.position_test.predicate_type.nand": "NAND",
"immersive_weathering.position_test.predicate_type.precipitation_test": "Precipitation test",
"immersive_weathering.position_test.predicate_type.temperature_range": "Temperature range",
"immersive_weathering.position_test.biome_match.biomes": "Biomes",
"immersive_weathering.position_test.day_test.day": "Day",
"immersive_weathering.position_test.nand.predicates": "Predicates",
"immersive_weathering.position_test.precipitation_test.precipitation": "Precipitation",
"immersive_weathering.position_test.temperature_range.min": "Min",
"immersive_weathering.position_test.temperature_range.max": "Max",
"immersive_weathering.position_test.temperature_range.use_local_pos": "Use local pos",
"immersive_weathering:rule_test.always_true": "Always true",
"immersive_weathering:rule_test.block_match": "Block match",
"immersive_weathering:rule_test.blockstate_match": "Block state match",
"immersive_weathering:rule_test.random_block_match": "Random block match",
"immersive_weathering:rule_test.random_blockstate_match": "Random block state match",
"immersive_weathering:rule_test.tag_match": "Tag match",
"immersive_weathering:rule_test.immersive_weathering:block_set_match": "Block set match",
"immersive_weathering:rule_test.immersive_weathering:fluid_match": "Fluid match",
"immersive_weathering:rule_test.immersive_weathering:tree_log": "Tree log",
"lithostitched:modifier_type.lithostitched:add_biome_spawns": "Add biome spawns",
"lithostitched:modifier_type.lithostitched:add_features": "Add features",
"lithostitched:modifier_type.lithostitched:add_pool_aliases": "Add pool aliases",
"lithostitched:modifier_type.lithostitched:add_structure_set_entries": "Add structure set entries",
"lithostitched:modifier_type.lithostitched:add_surface_rule": "Add surface rule",
"lithostitched:modifier_type.lithostitched:add_template_pool_elements": "Add template pool elements",
"lithostitched:modifier_type.lithostitched:no_op": "Nothing",
"lithostitched:modifier_type.lithostitched:redirect_feature": "Redirect feature",
"lithostitched:modifier_type.lithostitched:remove_biome_spawns": "Remove biome spawns",
"lithostitched:modifier_type.lithostitched:remove_features": "Remove features",
"lithostitched:modifier_type.lithostitched:remove_structures_from_structure_set": "Remove structures from set",
"lithostitched:modifier_type.lithostitched:replace_climate": "Replace climate",
"lithostitched:modifier_type.lithostitched:replace_effects": "Replace effects",
"lithostitched:modifier_predicate_type.lithostitched:all_of": "All of",
"lithostitched:modifier_predicate_type.lithostitched:any_of": "Any of",
"lithostitched:modifier_predicate_type.lithostitched:mod_loaded": "Mod loaded",
"lithostitched:modifier_predicate_type.lithostitched:not": "Not",
"lithostitched:modifier_predicate_type.lithostitched:true": "True",
"neoforge:biome_modifier_type.neoforge:none": "Disable Biome Modifier",
"neoforge:biome_modifier_type.neoforge:add_features": "Add Features",
"neoforge:biome_modifier_type.neoforge:remove_features": "Remove Features",
"neoforge:biome_modifier_type.neoforge:add_spawns": "Add Mob Spawns",
"neoforge:biome_modifier_type.neoforge:remove_spawns": "Remove Mob Spawns",
"neoforge:biome_modifier_type.neoforge:add_carvers": "Add World Carvers",
"neoforge:biome_modifier_type.neoforge:remove_carvers": "Remove World Carvers",
"neoforge:biome_modifier_type.neoforge:add_spawn_costs": "Add Mob Spawn Costs",
"neoforge:biome_modifier_type.neoforge:remove_spawn_costs": "Remove Mob Spawn Costs",
"neoforge:structure_modifier_type.neoforge:none": "Disable Structure Modifier",
"neoforge:structure_modifier_type.neoforge:add_spawns": "Add Mob Spawns",
"neoforge:structure_modifier_type.neoforge:remove_spawns": "Remove Mob Spawns",
"neoforge:structure_modifier_type.neoforge:clear_spawns": "Clear Mob Spawns",
"obsidian:item.information": "Item Information",
"obsidian:item_information.rarity": "Rarity",
"obsidian:item_information.creative_tab": "Creative Tab",
"obsidian:item_information.max_stack_size": "Max Stack Size",
"obsidian:item_information.name": "Item Name",
"obsidian:item_information.has_enchantment_glint": "Has Enchantment Glint",
"obsidian:item_information.is_enchantable": "Is Enchantable",
"obsidian:item_information.enchantability": "Enchantability",
"obsidian:item_information.use_duration": "Item Duration",
"obsidian:item_information.can_place_block": "Can Place Block",
"obsidian:item_information.placable_block": "Placable Block",
"obsidian:item_information.wearable": "Wearable",
"obsidian:item_information.default_color": "Default Color",
"obsidian:item_information.wearable_slot": "Wearable Slot",
"obsidian:item_information.custom_render_mode": "Has display-based models",
"obsidian:item_information.render_mode_models": "Display-based Models",
"obsidian:item.display": "Display",
"obsidian:item.display.model": "Model",
"obsidian:item.display.item_model": "Item Model",
"obsidian:item.display.lore": "Lore",
"obsidian:item.use_action": "Use Action",
"obsidian:item.use_action.action": "Action",
"obsidian:item.use_action.right_click_action": "Right Click Action",
"obsidian:item.use_action.command": "Command",
"obsidian:item.use_action.url": "URL",
"obsidian:item.use_action.gui_size": "GUI Size",
"obsidian:item.use_action.gui_title": "GUI Title",
"obsidian:block.block_type": "Block Type",
"obsidian:block.information": "Block Information",
"obsidian:block_information.rarity": "Rarity",
"obsidian:block_information.creative_tab": "Creative Tab",
"obsidian:block_information.max_stack_size": "Max Stack Size",
"obsidian:block_information.name": "Item Name",
"obsidian:block_information.sound_group_type": "Sound Group Type",
"obsidian:block_information.vanilla_sound_group": "Vanilla Sound Group",
"obsidian:block_information.custom_sound_group": "Custom Sound Group",
"obsidian:block_information.material_type": "Material Type",
"obsidian:block_information.vanilla_material": "Vanilla Material",
"obsidian:block_information.custom_material": "Custom Material",
"obsidian:block.display": "Display",
"obsidian:block.additional_information": "Additional Information",
"obsidian:block.ore_information": "Ore Information",
"obsidian:block.food_information": "Food Information",
"obsidian:block.block_type.CAMPFIRE.campfire_properties": "Campfire Properties",
"obsidian:block.can_plant_on": "Can Plant On",
"obsidian:block.particle_type": "Particle Type",
"obsidian:block.growable": "Growable",
"obsidian:block.oxidizable_properties": "Oxidizable Properties",
"obsidian:block.events": "Events",
"obsidian:block.drop_information": "Drop Information",
"obsidian:block.is_multi_block": "Is Multi-Block",
"obsidian:block.multiblock_information": "Multiblock Information",
"obsidian:block.placable_feature": "Placable Feature",
"obsidian:block.display.model": "Model",
"obsidian:block.display.item_model": "Item Model",
"obsidian:block.display.block_model": "Block Model",
"obsidian:block.display.lore": "Lore",
"ohthetreesyoullgrow:feature.ohthetreesyoullgrow:tree_from_nbt_v1": "Tree from NBT v1",
"ohthetreesyoullgrow.configured_feature.type": "Type",
"ohthetreesyoullgrow.configured_feature.config": "Config",
"ohthetreesyoullgrow.configured_feature.config.base_location": "Base location",
"ohthetreesyoullgrow.configured_feature.config.base_location.help": "The path to the trunk structure piece.",
"ohthetreesyoullgrow.configured_feature.config.canopy_location": "Canopy location",
"ohthetreesyoullgrow.configured_feature.config.canopy_location.help": "The path to the canopy structure piece.",
"ohthetreesyoullgrow.configured_feature.config.can_grow_on_filter": "Can grow on filter",
"ohthetreesyoullgrow.configured_feature.config.can_grow_on_filter.help": "Block filter for which this tree is allowed to grow on. Checks all of the red wool positions defined by the trunk.",
"ohthetreesyoullgrow.configured_feature.config.can_leaves_place_filter": "Can leaves place filter",
"ohthetreesyoullgrow.configured_feature.config.can_leaves_place_filter.help": "Block filter for which this tree's leaves are allowed to place.",
"ohthetreesyoullgrow.configured_feature.config.decorators": "Decorators",
"ohthetreesyoullgrow.configured_feature.config.decorators.entry": "Decorator",
"ohthetreesyoullgrow.configured_feature.config.height": "Height",
"ohthetreesyoullgrow.configured_feature.config.height.help": "Int provider defining the height of the tree.",
"ohthetreesyoullgrow.configured_feature.config.leaves_provider": "Leaves provider",
"ohthetreesyoullgrow.configured_feature.config.leaves_target": "Leaves target",
"ohthetreesyoullgrow.configured_feature.config.leaves_target.entry": "Block",
"ohthetreesyoullgrow.configured_feature.config.log_provider": "Log provider",
"ohthetreesyoullgrow.configured_feature.config.log_target": "Log target",
"ohthetreesyoullgrow.configured_feature.config.log_target.entry": "Block",
"ohthetreesyoullgrow.configured_feature.config.max_log_depth": "Max log depth",
"ohthetreesyoullgrow.configured_feature.config.place_from_nbt": "Place from NBT",
"ohthetreesyoullgrow.configured_feature.config.place_from_nbt.help": "Additional blocks from the structure pieces that should be placed in the world.",
"ohthetreesyoullgrow.configured_feature.config.place_from_nbt.entry": "Block"
}

View File

@@ -1,25 +0,0 @@
import type { INode, Path } from '@mcschema/core'
import { DataModel } from '@mcschema/core'
export class ModelWrapper extends DataModel {
constructor(
schema: INode<any>,
private readonly mapper: (path: Path) => Path,
private readonly getter: (path: Path) => any,
private readonly setter: (path: Path, value: any, silent?: boolean) => any,
) {
super(schema)
}
map(path: Path) {
return this.mapper(path)
}
get(path: Path) {
return this.getter(path)
}
set(path: Path, value: any, silent?: boolean) {
return this.setter(path, value, silent)
}
}

View File

@@ -1,719 +0,0 @@
import type { BooleanHookParams, EnumOption, Hook, INode, ListHookParams, NodeChildren, NumberHookParams, StringHookParams, ValidationOption } from '@mcschema/core'
import { DataModel, ListNode, MapNode, ModelPath, ObjectNode, Path, relativePath, StringNode } from '@mcschema/core'
import { Identifier, ItemStack } from 'deepslate/core'
import type { ComponentChildren, JSX } from 'preact'
import { memo } from 'preact/compat'
import { useState } from 'preact/hooks'
import { Btn, Octicon } from '../components/index.js'
import { ItemDisplay } from '../components/ItemDisplay.jsx'
import { VanillaColors } from '../components/previews/BiomeSourcePreview.jsx'
import config from '../Config.js'
import { localize, useLocale, useStore } from '../contexts/index.js'
import { useFocus } from '../hooks/index.js'
import type { BlockStateRegistry, VersionId } from '../services/index.js'
import { CachedDecorator, CachedFeature, checkVersion } from '../services/index.js'
import { deepClone, deepEqual, generateColor, generateUUID, hexId, hexToRgb, isObject, newSeed, rgbToHex, stringToColor } from '../Utils.js'
import { ModelWrapper } from './ModelWrapper.js'
const selectRegistries = ['loot_table.type', 'loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'recipe.type', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'dimension.generator.biome_source.preset', 'carver.type', 'feature.type', 'decorator.type', 'feature.tree.minimum_size.type', 'block_state_provider.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'structure_feature.type', 'surface_builder.type', 'processor.processor_type', 'rule_test.predicate_type', 'pos_rule_test.predicate_type', 'template_element.element_type', 'block_placer.type', 'block_predicate.type', 'material_rule.type', 'material_condition.type', 'structure_placement.type', 'density_function.type', 'root_placer.type', 'entity.type_specific.cat.variant', 'entity.type_specific.frog.variant', 'rule_block_entity_modifier.type', 'pool_alias_binding.type', 'lithostitched.worldgen_modifier.type', 'lithostitched.modifier_predicate.type', 'ohthetreesyoullgrow.configured_feature.type', 'enchantment_provider.type', 'enchantment_value_effect.type', 'level_based_value.type', 'neoforge.biome_modifier.type', 'neoforge.structure_modifier.type', 'tint_source.type', 'item_model.condition.property', 'item_model.select.property', 'item_model.range_dispatch.property', 'special_item_model.type']
const datalistEnums = ['item_stack.components', 'function.set_components.components']
const hiddenFields = ['number_provider.type', 'score_provider.type', 'nbt_provider.type', 'int_provider.type', 'float_provider.type', 'height_provider.type', 'level_based_value.type']
const flattenedFields = ['feature.config', 'decorator.config', 'int_provider.value', 'float_provider.value', 'block_state_provider.simple_state_provider.state', 'block_state_provider.rotated_block_provider.state', 'block_state_provider.weighted_state_provider.entries.entry.data', 'rule_test.block_state', 'structure_feature.config', 'surface_builder.config', 'template_pool.elements.entry.element', 'decorator.block_survives_filter.state', 'material_rule.block.result_state', 'enchantment.effects.entry.effect']
const inlineFields = ['loot_entry.type', 'function.function', 'condition.condition', 'criterion.trigger', 'dimension.generator.type', 'dimension.generator.biome_source.type', 'feature.type', 'decorator.type', 'block_state_provider.type', 'feature.tree.minimum_size.type', 'trunk_placer.type', 'foliage_placer.type', 'tree_decorator.type', 'block_placer.type', 'rule_test.predicate_type', 'processor.processor_type', 'template_element.element_type', 'nbt_operation.op', 'number_provider.value', 'score_provider.name', 'score_provider.target', 'nbt_provider.source', 'nbt_provider.target', 'generator_biome.biome', 'block_predicate.type', 'material_rule.type', 'material_condition.type', 'density_function.type', 'root_placer.type', 'entity.type_specific.type', 'glyph_provider.type', 'sprite_source.type', 'rule_block_entity_modifier.type', 'immersive_weathering.area_condition.type', 'immersive_weathering.block_growth.growth_for_face.entry.direction', 'immersive_weathering.position_test.predicate_type', 'pool_alias_binding.type', 'item_stack.id', 'data_component.container.entry.slot', 'map_decoration.type', 'suspicious_stew_effect_instance.id', 'enchantment_value_effect.type', 'enchantment_effect.type', 'particle.type', 'item_model.type', 'special_item_model.type', 'tint_source.type']
const nbtFields = ['function.set_nbt.tag', 'advancement.display.icon.nbt', 'text_component_object.nbt', 'entity.nbt', 'block.nbt', 'item.nbt']
const fixedLists = ['generator_biome.parameters.temperature', 'generator_biome.parameters.humidity', 'generator_biome.parameters.continentalness', 'generator_biome.parameters.erosion', 'generator_biome.parameters.depth', 'generator_biome.parameters.weirdness', 'feature.end_spike.crystal_beam_target', 'feature.end_gateway.exit', 'decorator.block_filter.offset', 'block_predicate.has_sturdy_face.offset', 'block_predicate.inside_world_bounds.offset', 'block_predicate.matching_block_tag.offset', 'block_predicate.matching_blocks.offset', 'block_predicate.matching_fluids.offset', 'block_predicate.would_survive.offset', 'model_element.from', 'model_element.to', 'model_element.rotation.origin', 'model_element.faces.uv', 'item_transform.rotation', 'item_transform.translation', 'item_transform.scale', 'generator_structure.random_spread.locate_offset', 'pack_overlay.formats', 'data_component.profile.id', 'data_component.lodestone_tracker.tracker.pos', 'attribute_modifier.uuid', 'tint_source.constant.value', 'tint_source.dye.default', 'tint_source.firework.default', 'tint_source.potion.default', 'tint_source.map_color.default']
const collapsedFields = ['noise_settings.surface_rule', 'noise_settings.noise.terrain_shaper']
const collapsableFields = ['density_function.argument', 'density_function.argument1', 'density_function.argument2', 'density_function.input', 'density_function.when_in_range', 'density_function.when_out_of_range']
const itemPreviewFields = ['loot_pool.entries.entry', 'loot_entry.alternatives.children.entry', 'loot_entry.group.children.entry', 'loot_entry.sequence.children.entry', 'function.set_contents.entries.entry']
const forceEnumContexts: Record<string, string> = { 'loot_table.type': 'loot_table.type', 'condition.condition': 'loot_condition_type', 'function.function': 'loot_function_type' }
const findGenerator = (id: string) => {
return config.generators.find(g => g.id === id.replace(/^\$/, ''))
}
/**
* Secondary model used to remember the keys of a map
*/
const keysModel = new DataModel(MapNode(
StringNode(),
StringNode()
), { historyMax: 0 })
type JSXTriple = [JSX.Element | null, JSX.Element | null, JSX.Element | null]
type RenderHook = Hook<[any, string, VersionId, BlockStateRegistry, Record<string, any>], JSXTriple>
type NodeProps<T> = T & {
node: INode<any>,
path: ModelPath,
value: any,
lang: string,
version: VersionId,
states: BlockStateRegistry,
ctx: Record<string, any>,
}
export function FullNode({ model, lang, version, blockStates }: { model: DataModel, lang: string, version: VersionId, blockStates: BlockStateRegistry }) {
const path = new ModelPath(model)
const [prefix, suffix, body] = model.schema.hook(renderHtml, path, deepClone(model.data), lang, version, blockStates, {})
return suffix?.props?.children.some((c: any) => c) ? <div class={`node ${model.schema.type(path)}-node`} data-category={model.schema.category(path)}>
<div class="node-header">{prefix}{suffix}</div>
<div class="node-body">{body}</div>
</div> : body
}
const renderHtml: RenderHook = {
base() {
return [null, null, null]
},
boolean(params, path, value, lang, version, states, ctx) {
return [null, <BooleanSuffix {...{...params, path, value, lang, version, states, ctx}} />, null]
},
choice({ choices, config, switchNode }, path, value, lang, version, states, ctx) {
const choice = switchNode.activeCase(path, true) as typeof choices[number]
const contextPath = (config?.context) ? new ModelPath(path.getModel(), new Path(path.getArray(), [config.context])) : path
const [prefix, suffix, body] = choice.node.hook(this, contextPath, value, lang, version, states, ctx)
if (choices.length === 1) {
return [prefix, suffix, body]
}
const choiceContextPath = config?.choiceContext ? new Path([], [config.choiceContext]) : config?.context ? new Path([], [config.context]) : path
const set = (type: string) => {
const c = choices.find(c => c.type === type) ?? choice
const def = c.node.default()
const newValue = c.change
? c.change(DataModel.unwrapLists(value))
: config.choiceContext === 'feature' && def?.type === 'minecraft:decorated' ? def.config.feature : def
path.model.set(path, DataModel.wrapLists(newValue))
}
const inject = <select value={choice.type} onChange={(e) => set((e.target as HTMLSelectElement).value)}>
{choices.map(c => <option value={c.type}>
{pathLocale(lang, choiceContextPath.contextPush(c.type))}
</option>)}
</select>
return [prefix, <>{inject}{suffix}</>, body]
},
list({ children, config, node }, path, value, lang, version, states, ctx) {
const context = path.getContext().join('.')
if (fixedLists.includes(context)) {
const prefix = <>
{[...Array(config.maxLength!)].map((_, i) =>
<ErrorPopup lang={lang} path={path.modelPush(i)} />)}
<div class="fixed-list"></div>
</>
const suffix = <>{[...Array(config.maxLength)].map((_, i) => {
const child = children.hook(this, path.modelPush(i), value?.[i]?.node, lang, version, states, ctx)
return child[1]
})}</>
return [prefix, suffix, null]
}
const onAdd = () => {
if (!Array.isArray(value)) value = []
const node = DataModel.wrapLists(children.default())
path.model.set(path, [{ node, id: hexId() }, ...value])
}
const suffix = <button class="add tooltipped tip-se" aria-label={localize(lang, 'add_top')} onClick={onAdd}>{Octicon.plus_circle}</button>
return [null, suffix, <ListBody {...{children, config, node, path, value, lang, version, states, ctx}}/>]
},
map({ children, keys, config }, path, value, lang, version, states, ctx) {
const { expand, collapse, isToggled } = useToggles()
const keyPath = new ModelPath(keysModel, new Path([hashString(path.toString())], path.contextArr))
const onAdd = () => {
const key = keyPath.get()
if (path.model.get(path.push(key)) === undefined) {
path.model.set(path.push(key), DataModel.wrapLists(children.default()))
}
keyPath.set('')
}
const blockState = config.validation?.validator === 'block_state_map' ? states?.[relativePath(path, config.validation.params.id).get()] : null
const keysSchema = blockState?.properties
? StringNode(null!, { enum: Object.keys(blockState.properties ?? {}) })
: keys
if (blockState && path.last() === 'Properties') {
if (typeof value !== 'object') value = {}
const properties = Object.entries(blockState.properties ?? {})
.map(([key, values]) => [key, StringNode(null!, { enum: values })])
Object.entries(blockState.properties ?? {}).forEach(([key, values]) => {
if (typeof value[key] !== 'string') {
path.model.errors.add(path.push(key), 'error.expected_string')
} else if (!values.includes(value[key])) {
path.model.errors.add(path.push(key), 'error.invalid_enum_option', value[key])
}
})
return ObjectNode(Object.fromEntries(properties)).hook(this, path, value, lang, version, states, ctx)
}
const suffix = <>
{keysSchema.hook(this, keyPath, keyPath.get() ?? '', lang, version, states, ctx)[1]}
<button class="add tooltipped tip-se" aria-label={localize(lang, 'add')} onClick={onAdd}>{Octicon.plus_circle}</button>
</>
const body = <>
{typeof value === 'object' && Object.entries(value).map(([key, cValue]) => {
const pathWithContext = (config?.context) ? new ModelPath(path.getModel(), new Path(path.getArray(), [config.context])) : path
const cPath = pathWithContext.modelPush(key)
const canToggle = children.type(cPath) === 'object'
const toggle = isToggled(key)
if (canToggle && (toggle === false || (toggle === undefined && value.length > 20))) {
return <div class="node node-header" data-category={children.category(cPath)}>
<ErrorPopup lang={lang} path={cPath} nested />
<button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'expand')}\n${localize(lang, 'expand_all', 'Ctrl')}`} onClick={expand(key)}>{Octicon.chevron_right}</button>
<label>{key}</label>
<Collapsed key={key} path={cPath} value={cValue} schema={children} />
</div>
}
const cSchema = blockState
? StringNode(null!, { enum: blockState.properties?.[key] ?? [] })
: children
if (blockState?.properties?.[key] && typeof cValue === 'string'
&& !blockState.properties?.[key].includes(cValue)) {
path.model.errors.add(cPath, 'error.invalid_enum_option', cValue)
}
const onRemove = () => cPath.set(undefined)
return <MemoedTreeNode key={key} schema={cSchema} path={cPath} value={cValue} {...{lang, version, states, ctx}} label={key}>
{canToggle && <button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'collapse')}\n${localize(lang, 'collapse_all', 'Ctrl')}`} onClick={collapse(key)}>{Octicon.chevron_down}</button>}
<button class="remove tooltipped tip-se" aria-label={localize(lang, 'remove')} onClick={onRemove}>{Octicon.trashcan}</button>
</MemoedTreeNode>
})}
</>
return [null, suffix, body]
},
number(params, path, value, lang, version, states, ctx) {
return [null, <NumberSuffix {...{...params, path, value, lang, version, states, ctx}} />, null]
},
object({ node, config, getActiveFields, getChildModelPath }, path, value, lang, version, states, ctx) {
const { expand, collapse, isToggled } = useToggles()
if (path.getArray().length == 0 && isDecorated(config.context, value)) {
const { wrapper, fields } = createDecoratorsWrapper(getActiveFields(path), path, value)
value = wrapper.data
getActiveFields = () => fields
getChildModelPath = (path, key) => new ModelPath(wrapper, new Path(path.getArray(), ['feature'])).push(key)
}
let prefix: JSX.Element | null = null
let suffix: JSX.Element | null = null
let body: JSX.Element | null = null
if (node.optional()) {
if (value === undefined) {
const onExpand = () => path.set(DataModel.wrapLists(node.default()))
suffix = <button class="node-collapse closed tooltipped tip-se" aria-label={localize(lang, 'expand')} onClick={onExpand}>{Octicon.plus_circle}</button>
} else if (typeof value === 'object' && value !== null){
const onCollapse = () => path.set(undefined)
suffix = <button class="node-collapse open tooltipped tip-se" aria-label={localize(lang, 'remove')} onClick={onCollapse}>{Octicon.trashcan}</button>
}
}
const context = path.getContext().join('.')
if (collapsableFields.includes(context) || collapsedFields.includes(context)) {
const toggled = isToggled('')
const expanded = collapsedFields.includes(context) ? toggled : !toggled
prefix = <>
<button class="toggle tooltipped tip-se" aria-label={localize(lang, expanded ? 'collapse' : 'expand')} onClick={toggled ? collapse('') : expand('')}>{expanded ? Octicon.chevron_down : Octicon.chevron_right}</button>
</>
if (!expanded) {
return [prefix, suffix, null]
}
}
if (!(node.optional() && value === undefined)) {
if (typeof value === 'object' && value !== null) {
const newCtx = (typeof value === 'object' && value !== null && node.default()?.pools)
? { ...ctx, loot: value?.type } : ctx
body = <>{Object.entries(getActiveFields(path))
.filter(([_, child]) => child.enabled(path))
.map(([key, child]) => {
const cPath = getChildModelPath(path, key)
const context = cPath.getContext().join('.')
if (hiddenFields.includes(context)) return null
const [cPrefix, cSuffix, cBody] = child.hook(this, cPath, value[key], lang, version, states, newCtx)
const isFlattened = child.type(cPath) === 'object' && flattenedFields.includes(context)
const isInlined = inlineFields.includes(context)
if (isFlattened || isInlined) {
prefix = <>{prefix}<ErrorPopup lang={lang} path={cPath} /><HelpPopup lang={lang} path={cPath} />{cPrefix}</>
suffix = <>{suffix}{cSuffix}</>
return isFlattened ? cBody : null
}
return <MemoedTreeNode key={key} schema={child} path={cPath} value={value[key]} {...{lang, version, states, ctx: newCtx}} />
})}</>
} else {
const onReset = () => path.set(DataModel.wrapLists(node.default()))
suffix = <>{suffix}<button class="add tooltipped tip-se" aria-label={localize(lang, 'reset')} onClick={onReset}>{Octicon.history}</button></>
}
}
return [prefix, suffix, body]
},
string(params, path, value, lang, version, states, ctx) {
return [null, <StringSuffix {...{...params, path, value, lang, version, states, ctx}} />, null]
},
}
function Collapsed({ path, value }: { path: ModelPath, value: any, schema: INode<any> }) {
const { locale } = useLocale()
const context = path.getContext().join('.')
switch (context) {
case 'loot_table.pools.entry':
const count = value?.entries?.length ?? 0
return <label>{count} {count == 1 ? 'entry' : 'entries'}</label>
case 'function.set_contents.entries.entry':
case 'loot_pool.entries.entry':
const name = value?.name?.replace(/^minecraft:/, '') ?? value?.type?.replace(/^minecraft:/, '')
const weight = value?.weight || undefined
return <>
<label>{name}</label>
{weight !== undefined && <label class="tooltipped tip-se" aria-label={locale('weight')}>{weight}</label>}
</>
}
for (const child of Object.values(value ?? {})) {
if (typeof child === 'string') {
return <label>{child.replace(/^minecraft:/, '')}</label>
}
}
return null
}
function useToggles() {
const [toggleState, setToggleState] = useState(new Map<string, boolean>())
const [toggleAll, setToggleAll] = useState<boolean | undefined>(undefined)
const expand = (key: string) => (evt: MouseEvent) => {
if (evt.ctrlKey) {
setToggleState(new Map())
setToggleAll(true)
} else {
setToggleState(state => new Map(state.set(key, true)))
}
}
const collapse = (key: string) => (evt: MouseEvent) => {
if (evt.ctrlKey) {
setToggleState(new Map())
setToggleAll(false)
} else {
setToggleState(state => new Map(state.set(key, false)))
}
}
const isToggled = (key: string) => {
if (!(toggleState instanceof Map)) return false
return toggleState.get(key) ?? toggleAll
}
return { expand, collapse, isToggled }
}
function ListBody({ path, value, lang, config, children, version, states, ctx }: NodeProps<ListHookParams>) {
const { expand, collapse, isToggled } = useToggles()
const [maxShown, setMaxShown] = useState(50)
const onAddBottom = () => {
if (!Array.isArray(value)) value = []
const node = DataModel.wrapLists(children.default())
path.model.set(path, [...value, { node, id: hexId() }])
}
return <>
{(value && Array.isArray(value)) && value.map(({ node: cValue, id: cId }, index) => {
if (index === maxShown) {
return <div class="node node-header">
<label>{localize(lang, 'entries_hidden', `${value.length - maxShown}`)}</label>
<button onClick={() => setMaxShown(Math.min(maxShown + 50, value.length))}>{localize(lang, 'entries_hidden.more', '50')}</button>
<button onClick={() => setMaxShown(value.length)}>{localize(lang, 'entries_hidden.all')}</button>
</div>
}
if (index > maxShown) {
return null
}
const pathWithContext = (config?.context) ? new ModelPath(path.getModel(), new Path(path.getArray(), [config.context])) : path
const cPath = pathWithContext.push(index).contextPush('entry')
const canToggle = children.type(cPath) === 'object'
const toggle = isToggled(cId)
let label: undefined | string | JSX.Element
if (itemPreviewFields.includes(cPath.getContext().join('.'))) {
if (isObject(cValue) && typeof cValue.type === 'string' && cValue.type.replace(/^minecraft:/, '') === 'item' && typeof cValue.name === 'string') {
let itemStack: ItemStack | undefined
try {
itemStack = new ItemStack(Identifier.parse(cValue.name), 1)
} catch (e) {}
if (itemStack !== undefined) {
label = <ItemDisplay item={itemStack} />
}
}
}
if (canToggle && (toggle === false || (toggle === undefined && value.length > 20))) {
return <div class="node node-header" data-category={children.category(cPath)}>
<ErrorPopup lang={lang} path={cPath} nested />
<button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'expand')}\n${localize(lang, 'expand_all', 'Ctrl')}`} onClick={expand(cId)}>{Octicon.chevron_right}</button>
<label>{label ?? pathLocale(lang, cPath, `${index}`)}</label>
<Collapsed key={cId} path={cPath} value={cValue} schema={children} />
</div>
}
const onRemove = () => cPath.set(undefined)
const onMoveUp = () => {
const v = [...path.get()];
[v[index - 1], v[index]] = [v[index], v[index - 1]]
path.model.set(path, v)
}
const onMoveDown = () => {
const v = [...path.get()];
[v[index + 1], v[index]] = [v[index], v[index + 1]]
path.model.set(path, v)
}
const actions: MenuAction[] = [
{
icon: 'duplicate',
label: 'duplicate',
onSelect: () => {
const v = [...path.get()]
v.splice(index, 0, { id: hexId(), node: deepClone(cValue) })
path.model.set(path, v)
},
},
]
return <MemoedTreeNode key={cId} label={label} path={cPath} schema={children} value={cValue} {...{lang, version, states, actions}} ctx={{...ctx, index: (index === 0 ? 1 : 0) + (index === value.length - 1 ? 2 : 0)}}>
{canToggle && <button class="toggle tooltipped tip-se" aria-label={`${localize(lang, 'collapse')}\n${localize(lang, 'collapse_all', 'Ctrl')}`} onClick={collapse(cId)}>{Octicon.chevron_down}</button>}
<button class="remove tooltipped tip-se" aria-label={localize(lang, 'remove')} onClick={onRemove}>{Octicon.trashcan}</button>
{value.length > 1 && <div class="node-move">
<button class="move tooltipped tip-se" aria-label={localize(lang, 'move_up')} onClick={onMoveUp} disabled={index === 0}>{Octicon.chevron_up}</button>
<button class="move tooltipped tip-se" aria-label={localize(lang, 'move_down')} onClick={onMoveDown} disabled={index === value.length - 1}>{Octicon.chevron_down}</button>
</div>}
</MemoedTreeNode>
})}
{(value && value.length > 0 && value.length <= maxShown) && <div class="node node-header">
<button class="add tooltipped tip-se" aria-label={localize(lang, 'add_bottom')} onClick={onAddBottom}>{Octicon.plus_circle}</button>
</div>}
</>
}
function BooleanSuffix({ path, node, value, lang }: NodeProps<BooleanHookParams>) {
const set = (target: boolean) => {
path.model.set(path, node.optional() && value === target ? undefined : target)
}
return <>
<button class={value === false ? 'selected' : ''} onClick={() => set(false)}>{localize(lang, 'false')}</button>
<button class={value === true ? 'selected' : ''} onClick={() => set(true)}>{localize(lang, 'true')}</button>
</>
}
function NumberSuffix({ path, config, integer, value, lang }: NodeProps<NumberHookParams>) {
const onChange = (evt: Event) => {
const value = (evt.target as HTMLInputElement).value
const parsed = integer ? parseInt(value) : parseFloat(value)
path.model.set(path, parsed)
}
const onColor = (evt: Event) => {
const value = (evt.target as HTMLInputElement).value
const parsed = parseInt(value.slice(1), 16)
path.model.set(path, parsed)
}
return <>
<input type="text" value={value ?? ''} onBlur={onChange} onKeyDown={evt => {if (evt.key === 'Enter') onChange(evt)}} />
{config?.color && <input type="color" value={'#' + (value?.toString(16).padStart(6, '0') ?? '000000')} onChange={onColor} />}
{config?.color && <button onClick={() => path.set(generateColor())} class="tooltipped tip-se" aria-label={localize(lang, 'generate_new_color')}>{Octicon.sync}</button>}
{['dimension.generator.seed', 'dimension.generator.biome_source.seed', 'world_settings.seed', 'structure_placement.salt'].includes(path.getContext().join('.')) && <button onClick={() => newSeed(path.model)} class="tooltipped tip-se" aria-label={localize(lang, 'generate_new_seed')}>{Octicon.sync}</button>}
</>
}
function StringSuffix({ path, getValues, config, node, value, lang, version, states }: NodeProps<StringHookParams>) {
const context = path.getContext().join('.')
const onChange = (evt: Event) => {
evt.stopPropagation()
const newValue = (evt.target as HTMLSelectElement).value
if (newValue === value) return
// Hackfix to support switching between checkerboard and multi_noise biome sources
if (context === 'dimension.generator.biome_source.type') {
const biomeSourceType = newValue.replace(/^minecraft:/, '')
const biomePath = path.pop().push('biomes')
const biomes = biomePath.get()
if (biomeSourceType === 'multi_noise') {
const newBiomes = Array.isArray(biomes)
? biomes.flatMap((b: any) => {
if (typeof b.node !== 'string') return []
return [{ node: { biome: b.node }}]
})
: [{ node: { biome: 'minecraft:plains' } }]
path.model.set(biomePath, newBiomes, true)
} else if (biomeSourceType === 'checkerboard') {
const newBiomes = typeof biomes === 'string'
? biomes
: Array.isArray(biomes)
? biomes.flatMap((b: any) => {
if (typeof b.node !== 'object' || b.node === null || typeof b.node.biome !== 'string') return []
return [{ node: b.node.biome }]
})
: [{ node: 'minecraft:plains' }]
path.model.set(biomePath, newBiomes, true)
}
}
path.model.set(path, newValue.length === 0 ? undefined : newValue)
}
const values = getValues()
const id = !isEnum(config) && config?.validator === 'resource' && typeof config.params.pool === 'string' ? config.params.pool : undefined
if (nbtFields.includes(context)) {
return <textarea value={value ?? ''} onBlur={onChange}></textarea>
} else if ((isEnum(config) && !config.additional && !datalistEnums.includes(context)) || selectRegistries.includes(context)) {
let childPath = new Path([])
if (isEnum(config) && typeof config.enum === 'string') {
childPath = childPath.contextPush(config.enum)
} else if (id) {
childPath = childPath.contextPush(id)
} else if (isEnum(config)) {
childPath = path
} else if (Object.hasOwn(forceEnumContexts, context)) {
childPath = childPath.contextPush(forceEnumContexts[context])
}
return <select value={value ?? ''} onChange={onChange}>
{node.optional() && <option value="">{localize(lang, 'unset')}</option>}
{values.map(v => <option value={v}>
{pathLocale(lang, childPath.contextPush(v.replace(/^minecraft:/, '')))}
</option>)}
</select>
} else if (!isEnum(config) && config?.validator === 'block_state_key') {
const blockState = states?.[relativePath(path, config.params.id).get()]
const values = Object.keys(blockState?.properties ?? {})
return <select value={value ?? ''} onChange={onChange}>
{values.map(v => <option>{v}</option>)}
</select>
} else {
const { biomeColors, setBiomeColor } = useStore()
const fullId = typeof value === 'string' ? value.includes(':') ? value : 'minecraft:' + value : 'unknown'
const datalistId = hexId()
const gen = id ? findGenerator(id) : undefined
return <>
<input value={value ?? ''} onBlur={onChange} onKeyDown={evt => {if (evt.key === 'Enter') onChange(evt)}}
list={values.length > 0 ? datalistId : ''} />
{values.length > 0 && <datalist id={datalistId}>
{values.map(v => <option value={v} />)}
</datalist>}
{['generator_biome.biome'].includes(context) && <input type="color" value={rgbToHex(biomeColors[fullId] ?? VanillaColors[fullId] ?? stringToColor(fullId))} onChange={v => setBiomeColor(fullId, hexToRgb(v.currentTarget.value))}></input>}
{(['text_component_object.hoverEvent.show_entity.contents.id', 'enchantment.effects.entry.uuid'].includes(context) || ('attribute_modifier.id' === context && !checkVersion(version, '1.21'))) && <button onClick={() => path.set(generateUUID())} class="tooltipped tip-se" aria-label={localize(lang, 'generate_new_uuid')}>{Octicon.sync}</button>}
{gen && values.includes(value) && value.startsWith('minecraft:') &&
<a href={`/${gen.url}/?version=${version}&preset=${value.replace(/^minecraft:/, '')}`} class="tooltipped tip-se" aria-label={localize(lang, 'follow_reference')}>{Octicon.link_external}</a>}
</>
}
}
type MenuAction = {
label: string,
description?: string,
icon: keyof typeof Octicon,
onSelect: () => unknown,
}
type TreeNodeProps = {
schema: INode<any>,
path: ModelPath,
value: any,
lang: string,
version: VersionId,
states: BlockStateRegistry,
ctx: Record<string, any>,
compare?: any,
label?: string | ComponentChildren,
actions?: MenuAction[],
children?: ComponentChildren,
}
function TreeNode({ label, schema, path, value, lang, version, states, ctx, actions, children }: TreeNodeProps) {
const type = schema.type(path)
const category = schema.category(path)
const context = path.getContext().join('.')
const [active, setActive] = useFocus()
const onContextMenu = (evt: MouseEvent) => {
evt.preventDefault()
setActive()
}
const newCtx: Record<string, any> = { ...ctx, depth: (ctx.depth ?? 0) + 1 }
delete newCtx.index
const [prefix, suffix, body] = schema.hook(renderHtml, path, value, lang, version, states, newCtx)
return <div class={`node ${type}-node`} data-category={category}>
<div class="node-header" onContextMenu={onContextMenu}>
<ErrorPopup lang={lang} path={path} />
<HelpPopup lang={lang} path={path} />
{children}
{prefix}
<label>
{label ?? pathLocale(lang, path, `${path.last()}`)}
{active && <div class="node-menu">
{actions?.map(a => <div key={a.label} class="menu-item">
<Btn icon={a.icon} tooltip={localize(lang, a.label)} tooltipLoc="se" onClick={() => a.onSelect()}/>
<span>{a.description ?? localize(lang, a.label)}</span>
</div>)}
<div class="menu-item">
<Btn icon="copy" tooltip={localize(lang, 'copy_context')} tooltipLoc="se" onClick={() => navigator.clipboard.writeText(context)} />
<span>{context}</span>
</div>
</div>}
</label>
{suffix}
</div>
{body && <div class="node-body">{body}</div>}
</div>
}
const MemoedTreeNode = memo(TreeNode, (prev, next) => {
return prev.schema === next.schema
&& prev.lang === next.lang
&& prev.path.equals(next.path)
&& deepEqual(prev.ctx, next.ctx)
&& deepEqual(prev.value, next.value)
})
function isEnum(value?: ValidationOption | EnumOption): value is EnumOption {
return !!(value as any)?.enum
}
function hashString(str: string) {
var hash = 0, i, chr
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i)
hash = ((hash << 5) - hash) + chr
hash |= 0
}
return hash
}
function pathLocale(lang: string, path: Path, ...params: string[]) {
const ctx = path.getContext()
for (let i = 0; i < ctx.length; i += 1) {
const key = ctx.slice(i).join('.')
const result = localize(lang, key, ...params)
if (key !== result) {
return result
}
}
return ctx[ctx.length - 1]
}
function ErrorPopup({ lang, path, nested }: { lang: string, path: ModelPath, nested?: boolean }) {
if (path.model instanceof ModelWrapper) {
path = path.model.map(path).withModel(path.model)
}
const e = nested
? path.model.errors.getAll().filter(e => e.path.startsWith(path))
: path.model.errors.get(path, true)
if (e.length === 0) return null
const message = localize(lang, e[0].error, ...(e[0].params ?? []))
return popupIcon('node-error', 'issue_opened', message)
}
function HelpPopup({ lang, path }: { lang: string, path: Path }) {
const key = path.contextPush('help').getContext().join('.')
const message = localize(lang, key)
if (message === key) return null
return popupIcon('node-help', 'info', message)
}
const popupIcon = (type: string, icon: keyof typeof Octicon, popup: string) => {
const [active, setActive] = useFocus()
return <div class={`node-icon ${type}${active ? ' show' : ''}`} onClick={() => setActive()}>
{Octicon[icon]}
<span class="icon-popup">{popup}</span>
</div>
}
function isDecorated(context: string | undefined, value: any) {
return context === 'feature'
&& value?.type?.replace(/^minecraft:/, '') === 'decorated'
&& isObject(value?.config)
}
function createDecoratorsWrapper(originalFields: NodeChildren, path: ModelPath, value: any) {
const decorators: any[] = []
const feature = iterateNestedDecorators(value, decorators)
const fields = {
type: originalFields.type,
config: ObjectNode({
decorators: ListNode(CachedDecorator),
feature: CachedFeature,
}, { context: 'feature.decorated' }),
}
const schema = ObjectNode(fields, { context: 'feature' })
const featurePath = new Path(['config', 'feature'])
const decoratorsPath = new Path(['config', 'decorators'])
const model = path.getModel()
const wrapper: ModelWrapper = new ModelWrapper(schema, path => {
if (path.startsWith(featurePath)) {
return new Path([...[...Array(decorators.length - 1)].flatMap(() => ['config', 'feature']), ...path.modelArr])
} else if (path.startsWith(decoratorsPath)) {
if (path.modelArr.length === 2) {
return new Path([])
}
const index = path.modelArr[2]
if (typeof index === 'number') {
return new Path([...[...Array(index)].flatMap(() => ['config', 'feature']), 'config', 'decorator', ...path.modelArr.slice(3)])
}
}
return path
}, path => {
if (path.equals(decoratorsPath)) {
const newDecorators: any[] = []
iterateNestedDecorators(model.data, newDecorators)
return newDecorators
}
return model.get(wrapper.map(path))
}, (path, value, silent) => {
if (path.startsWith(featurePath)) {
const newDecorators: any[] = []
iterateNestedDecorators(model.data, newDecorators)
const newPath =new Path([...[...Array(newDecorators.length - 1)].flatMap(() => ['config', 'feature']), ...path.modelArr])
return model.set(newPath, value, silent)
} else if (path.startsWith(decoratorsPath)) {
const index = path.modelArr[2]
if (path.modelArr.length === 2) {
const feature = wrapper.get(featurePath)
return model.set(new Path(), produceNestedDecorators(feature, value), silent)
} else if (typeof index === 'number') {
if (path.modelArr.length === 3 && value === undefined) {
const feature = wrapper.get(featurePath)
const newDecorators: any[] = []
iterateNestedDecorators(model.data, newDecorators)
newDecorators.splice(index, 1)
const newValue = produceNestedDecorators(feature, newDecorators)
return model.set(new Path(), newValue, silent)
} else {
const newPath = new Path([...[...Array(index)].flatMap(() => ['config', 'feature']), 'config', 'decorator', ...path.modelArr.slice(3)])
return model.set(newPath, value, silent)
}
}
}
model.set(path, value, silent)
})
wrapper.data = {
type: model.data.type,
config: {
decorators,
feature,
},
}
wrapper.errors = model.errors
return { fields, wrapper }
}
function iterateNestedDecorators(value: any, decorators: any[]): any {
if (value?.type?.replace(/^minecraft:/, '') !== 'decorated') {
return value
}
if (!isObject(value?.config)) {
return value
}
decorators.push({ id: decorators.length, node: value.config.decorator })
return iterateNestedDecorators(value.config.feature ?? '', decorators)
}
function produceNestedDecorators(feature: any, decorators: any[]): any {
if (decorators.length === 0) return feature
return {
type: 'minecraft:decorated',
config: {
decorator: decorators.shift().node,
feature: produceNestedDecorators(feature, decorators),
},
}
}

View File

@@ -1,67 +0,0 @@
import type { DataModel, Hook } from '@mcschema/core'
import { ModelPath, relativePath } from '@mcschema/core'
import type { BlockStateRegistry } from '../services/index.js'
export function getOutput(model: DataModel, blockStates: BlockStateRegistry): any {
return model.schema.hook(transformOutput, new ModelPath(model), model.data, { blockStates })
}
export type OutputProps = {
blockStates: BlockStateRegistry,
}
export const transformOutput: Hook<[any, OutputProps], any> = {
base({}, _, value) {
return value
},
choice({ switchNode }, path, value, props) {
return switchNode.hook(this, path, value, props)
},
list({ children }, path, value, props) {
if (!Array.isArray(value)) return value
const res = value.map((obj, index) =>
children.hook(this, path.push(index), obj.node, props)
)
for (const a of Object.getOwnPropertySymbols(value)) {
res[a as any] = value[a as any]
}
return res
},
map({ children, config }, path, value, props) {
if (value === undefined) return undefined
const blockState = config.validation?.validator === 'block_state_map'? props.blockStates?.[relativePath(path, config.validation.params.id).get()] : null
const res: any = {}
Object.keys(value).forEach(f => {
if (blockState) {
if (!Object.keys(blockState.properties ?? {}).includes(f)) return
}
res[f] = children.hook(this, path.push(f), value[f], props)
})
for (const a of Object.getOwnPropertySymbols(value)) {
res[a as any] = value[a]
}
return res
},
object({ getActiveFields }, path, value, props) {
if (value === undefined || value === null || typeof value !== 'object') {
return value
}
const res: any = {}
const activeFields = getActiveFields(path)
Object.keys(activeFields)
.filter(k => activeFields[k].enabled(path))
.forEach(f => {
const out = activeFields[f].hook(this, path.push(f), value[f], props)
if (out !== undefined && out !== null) {
res[f] = out
}
})
for (const a of Object.getOwnPropertySymbols(value)) {
res[a as any] = value[a]
}
return res
},
}

View File

@@ -1,20 +1,13 @@
import type { CollectionRegistry } from '@mcschema/core'
import config from '../Config.js'
import { Store } from '../Store.js'
import { message } from '../Utils.js'
import type { BlockStateRegistry, VersionId } from './Schemas.js'
import { checkVersion } from './Schemas.js'
import type { VersionId } from './Versions.js'
import { checkVersion } from './Versions.js'
const CACHE_NAME = 'misode-v2'
const CACHE_LATEST_VERSION = 'cached_latest_version'
const CACHE_PATCH = 'misode_cache_patch'
type Version = {
id: string,
ref?: string,
dynamic?: boolean,
}
declare var __LATEST_VERSION__: string
export const latestVersion = __LATEST_VERSION__ ?? ''
const mcmetaUrl = 'https://raw.githubusercontent.com/misode/mcmeta'
@@ -23,6 +16,7 @@ const changesUrl = 'https://raw.githubusercontent.com/misode/technical-changes'
const fixesUrl = 'https://raw.githubusercontent.com/misode/mcfixes'
const versionDiffUrl = 'https://mcmeta-diff.misode.workers.dev'
const whatsNewUrl = 'https://whats-new.misode.workers.dev'
const vanillaMcdocUrl = 'https://proxy.misode.workers.dev/mcdoc'
type McmetaTypes = 'summary' | 'data' | 'data-json' | 'assets' | 'assets-json' | 'registries' | 'atlas'
@@ -46,51 +40,35 @@ async function validateCache(version: RefInfo) {
}
}
export async function fetchData(versionId: string, collectionTarget: CollectionRegistry, blockStateTarget: BlockStateRegistry) {
const version = config.versions.find(v => v.id === versionId) as Version | undefined
if (!version) {
console.error(`[fetchData] Unknown version ${version} in ${JSON.stringify(config.versions)}`)
return
export function getVersionChecksum(versionId: VersionId) {
const version = config.versions.find(v => v.id === versionId)!
if (version.dynamic) {
return (localStorage.getItem(CACHE_LATEST_VERSION) ?? '').toString()
}
await validateCache(version)
await Promise.all([
_fetchRegistries(version, collectionTarget),
_fetchBlockStateMap(version, blockStateTarget),
])
return version.ref
}
async function _fetchRegistries(version: Version, target: CollectionRegistry) {
console.debug(`[fetchRegistries] ${version.id}`)
export async function fetchVanillaMcdoc() {
try {
const data = await cachedFetch<any>(`${mcmeta(version, 'summary')}/registries/data.min.json`)
for (const id in data) {
target.register(id, data[id].map((e: string) => 'minecraft:' + e))
}
// TODO: enable refresh
return cachedFetch(vanillaMcdocUrl, { decode: res => res.arrayBuffer(), refresh: false })
} catch (e) {
console.warn('Error occurred while fetching registries:', message(e))
throw new Error(`Error occured while fetching vanilla-mcdoc: ${message(e)}`)
}
}
async function _fetchBlockStateMap(version: Version, target: BlockStateRegistry) {
console.debug(`[fetchBlockStateMap] ${version.id}`)
export async function fetchDependencyMcdoc(dependency: string) {
try {
const data = await cachedFetch<any>(`${mcmeta(version, 'summary')}/blocks/data.min.json`)
for (const id in data) {
target['minecraft:' + id] = {
properties: data[id][0],
default: data[id][1],
}
}
return cachedFetch(`/mcdoc/${dependency}.mcdoc`, { decode: res => res.text(), refresh: true })
} catch (e) {
console.warn('Error occurred while fetching block state map:', message(e))
throw new Error(`Error occured while fetching ${dependency} mcdoc: ${message(e)}`)
}
}
export async function fetchRegistries(versionId: VersionId) {
console.debug(`[fetchRegistries] ${versionId}`)
const version = config.versions.find(v => v.id === versionId)!
await validateCache(version)
try {
const data = await cachedFetch<any>(`${mcmeta(version, 'summary')}/registries/data.min.json`)
const result = new Map<string, string[]>()
@@ -99,21 +77,21 @@ export async function fetchRegistries(versionId: VersionId) {
}
return result
} catch (e) {
throw new Error(`Error occurred while fetching registries (2): ${message(e)}`)
throw new Error(`Error occurred while fetching registries: ${message(e)}`)
}
}
export type BlockStateData = [Record<string, string[]>, Record<string, string>]
export async function fetchBlockStates(versionId: VersionId) {
console.debug(`[fetchBlockStates] ${versionId}`)
const version = config.versions.find(v => v.id === versionId)!
const result = new Map<string, {properties: Record<string, string[]>, default: Record<string, string>}>()
const result = new Map<string, BlockStateData>()
await validateCache(version)
try {
const data = await cachedFetch<any>(`${mcmeta(version, 'summary')}/blocks/data.min.json`)
for (const id in data) {
result.set('minecraft:' + id, {
properties: data[id][0],
default: data[id][1],
})
result.set(id, data[id])
}
} catch (e) {
console.warn('Error occurred while fetching block states:', message(e))
@@ -128,6 +106,7 @@ export async function fetchItemComponents(versionId: VersionId) {
if (!checkVersion(versionId, '1.20.5')) {
return result
}
await validateCache(version)
try {
const data = await cachedFetch<Record<string, Record<string, unknown>>>(`${mcmeta(version, 'summary')}/item_components/data.min.json`)
for (const [id, components] of Object.entries(data)) {
@@ -152,16 +131,17 @@ export async function fetchItemComponents(versionId: VersionId) {
export async function fetchPreset(versionId: VersionId, registry: string, id: string) {
console.debug(`[fetchPreset] ${versionId} ${registry} ${id}`)
const version = config.versions.find(v => v.id === versionId)!
await validateCache(version)
try {
let url
if (id.startsWith('immersive_weathering:')) {
url = `https://raw.githubusercontent.com/AstralOrdana/Immersive-Weathering/main/src/main/resources/data/immersive_weathering/block_growths/${id.slice(21)}.json`
} else {
const type = ['atlases', 'blockstates', 'items', 'models', 'font'].includes(registry) ? 'assets' : 'data'
const type = ['atlases', 'blockstates', 'items', 'font', 'lang', 'models', 'post_effect'].includes(registry) ? 'assets' : 'data'
url = `${mcmeta(version, type)}/${type}/minecraft/${registry}/${id}.json`
}
const res = await fetch(url)
return await res.json()
return await res.text()
} catch (e) {
throw new Error(`Error occurred while fetching ${registry} preset ${id}: ${message(e)}`)
}

View File

@@ -0,0 +1,279 @@
import * as core from '@spyglassmc/core'
import { message } from '../Utils.js'
// Copied from spyglass because it isn't exported
type Listener = (...args: any[]) => any
class BrowserEventEmitter implements core.ExternalEventEmitter {
readonly #listeners = new Map<string, { all: Set<Listener>, once: Set<Listener> }>()
emit(eventName: string, ...args: any[]): boolean {
const listeners = this.#listeners.get(eventName)
if (!listeners?.all?.size) {
return false
}
for (const listener of listeners.all) {
listener(...args)
if (listeners.once.has(listener)) {
listeners.all.delete(listener)
listeners.once.delete(listener)
}
}
return false
}
on(eventName: string, listener: Listener): this {
if (!this.#listeners.has(eventName)) {
this.#listeners.set(eventName, { all: new Set(), once: new Set() })
}
const listeners = this.#listeners.get(eventName)!
listeners.all.add(listener)
return this
}
once(eventName: string, listener: Listener): this {
if (!this.#listeners.has(eventName)) {
this.#listeners.set(eventName, { all: new Set(), once: new Set() })
}
const listeners = this.#listeners.get(eventName)!
listeners.all.add(listener)
listeners.once.add(listener)
return this
}
}
export class IndexedDbFileSystem implements core.ExternalFileSystem {
public static readonly dbName = 'misode-spyglass-fs'
public static readonly dbVersion = 1
public static readonly storeName = 'files'
private readonly db: Promise<IDBDatabase>
private watcher: IndexedDbWatcher | undefined
constructor() {
this.db = new Promise((res, rej) => {
const request = indexedDB.open(IndexedDbFileSystem.dbName, IndexedDbFileSystem.dbVersion)
request.onerror = (e) => {
console.warn('Database error', message((e.target as any)?.error))
rej()
}
request.onsuccess = () => {
res(request.result)
}
request.onupgradeneeded = (event) => {
const db = (event.target as any).result as IDBDatabase
db.createObjectStore(IndexedDbFileSystem.storeName, { keyPath: 'uri' })
}
})
}
async chmod(_location: core.FsLocation, _mode: number): Promise<void> {
return
}
async mkdir(
location: core.FsLocation,
_options?: { mode?: number | undefined, recursive?: boolean | undefined } | undefined,
): Promise<void> {
location = core.fileUtil.ensureEndingSlash(location.toString())
const db = await this.db
return new Promise((res, rej) => {
const transaction = db.transaction(IndexedDbFileSystem.storeName, 'readwrite')
const store = transaction.objectStore(IndexedDbFileSystem.storeName)
const getRequest = store.get(location)
getRequest.onsuccess = () => {
const entry = getRequest.result
if (entry !== undefined) {
rej(new Error(`EEXIST: ${location}`))
} else {
const putRequest = store.put({ uri: location, type: 'directory' })
putRequest.onsuccess = () => {
res()
}
putRequest.onerror = () => {
rej()
}
}
}
getRequest.onerror = () => {
rej()
}
})
}
async readdir(location: core.FsLocation): Promise<{ name: string, isDirectory(): boolean, isFile(): boolean, isSymbolicLink(): boolean }[]> {
location = core.fileUtil.ensureEndingSlash(location.toString())
const db = await this.db
return new Promise((res, rej) => {
const transaction = db.transaction(IndexedDbFileSystem.storeName, 'readonly')
const store = transaction.objectStore(IndexedDbFileSystem.storeName)
const request = store.openCursor(IDBKeyRange.bound(location, location + '\uffff'))
const result: { name: string, isDirectory(): boolean, isFile(): boolean, isSymbolicLink(): boolean }[] = []
request.onsuccess = () => {
if (request.result) {
const entry = request.result.value
result.push({
name: request.result.key.toString(),
isDirectory: () => entry.type === 'directory',
isFile: () => entry.type === 'file',
isSymbolicLink: () => false,
})
request.result.continue()
} else {
res(result)
}
}
request.onerror = () => {
rej()
}
})
}
async readFile(location: core.FsLocation): Promise<Uint8Array> {
location = location.toString()
const db = await this.db
return new Promise((res, rej) => {
const transaction = db.transaction(IndexedDbFileSystem.storeName, 'readonly')
const store = transaction.objectStore(IndexedDbFileSystem.storeName)
const request = store.get(location)
request.onsuccess = () => {
const entry = request.result
if (!entry) {
rej(new Error(`ENOENT: ${location}`))
} else if (entry.type === 'directory') {
rej(new Error(`EISDIR: ${location}`))
} else {
res(entry.content)
}
}
request.onerror = () => {
rej()
}
})
}
async showFile(_location: core.FsLocation): Promise<void> {
throw new Error('showFile not supported on browser')
}
async stat(location: core.FsLocation): Promise<{ isDirectory(): boolean, isFile(): boolean }> {
location = location.toString()
const db = await this.db
return new Promise((res, rej) => {
const transaction = db.transaction(IndexedDbFileSystem.storeName, 'readonly')
const store = transaction.objectStore(IndexedDbFileSystem.storeName)
const request = store.get(location)
request.onsuccess = () => {
const entry = request.result
if (!entry) {
rej(new Error(`ENOENT: ${location}`))
} else {
res({
isDirectory: () => entry.type === 'directory',
isFile: () => entry.type === 'file',
})
}
}
request.onerror = () => {
rej()
}
})
}
async unlink(location: core.FsLocation): Promise<void> {
location = location.toString()
const db = await this.db
return new Promise((res, rej) => {
const transaction = db.transaction(IndexedDbFileSystem.storeName, 'readwrite')
const store = transaction.objectStore(IndexedDbFileSystem.storeName)
const getRequest = store.get(location)
getRequest.onsuccess = () => {
const entry = getRequest.result
if (!entry) {
rej(new Error(`ENOENT: ${location}`))
} else {
const deleteRequest = store.delete(location)
deleteRequest.onsuccess = () => {
this.watcher?.tryEmit('unlink', location)
res()
}
deleteRequest.onerror = () => {
rej()
}
}
}
getRequest.onerror = () => {
rej()
}
})
}
watch(locations: core.FsLocation[], _options: { usePolling?: boolean | undefined }): core.FsWatcher {
this.watcher = new IndexedDbWatcher(this.db, locations)
return this.watcher
}
async writeFile(
location: core.FsLocation,
data: string | Uint8Array,
_options?: { mode: number } | undefined,
): Promise<void> {
location = location.toString()
if (typeof data === 'string') {
data = new TextEncoder().encode(data)
}
const db = await this.db
return new Promise((res, rej) => {
const transaction = db.transaction(IndexedDbFileSystem.storeName, 'readwrite')
const store = transaction.objectStore(IndexedDbFileSystem.storeName)
const getRequest = store.get(location)
getRequest.onsuccess = () => {
const entry = getRequest.result
const putRequest = store.put({ uri: location, type: 'file', content: data })
putRequest.onsuccess = () => {
if (entry) {
this.watcher?.tryEmit('change', location)
} else {
this.watcher?.tryEmit('add', location)
}
res()
}
putRequest.onerror = () => {
rej()
}
}
getRequest.onerror = () => {
rej()
}
})
}
}
class IndexedDbWatcher extends BrowserEventEmitter implements core.FsWatcher {
constructor(
dbPromise: Promise<IDBDatabase>,
private readonly locations: core.FsLocation[],
) {
super()
dbPromise.then((db) => {
const transaction = db.transaction(IndexedDbFileSystem.storeName, 'readonly')
const store = transaction.objectStore(IndexedDbFileSystem.storeName)
const request = store.openKeyCursor()
request.onsuccess = () => {
if (request.result) {
const uri = request.result.key.toString()
this.tryEmit('add', uri)
request.result.continue()
} else {
this.emit('ready')
}
}
request.onerror = () => {
this.emit('error', new Error('Watcher error'))
}
})
}
tryEmit(eventName: string, uri: string) {
for (const location of this.locations) {
if (uri.startsWith(location)) {
this.emit(eventName, uri)
break
}
}
}
async close(): Promise<void> {}
}

View File

@@ -1,5 +1,6 @@
import type { NbtTag } from 'deepslate'
import { Identifier, ItemStack } from 'deepslate'
import { safeJsonParse } from '../Utils.js'
export class ResolvedItem extends ItemStack {
@@ -108,11 +109,7 @@ export class ResolvedItem extends ItemStack {
public getLore() {
return this.get('lore', tag => {
return tag.isList() ? tag.map(e => {
try {
return JSON.parse(e.getAsString())
} catch (e) {
return { text: '(invalid lore line)' }
}
return safeJsonParse(e.getAsString()) ?? { text: '(invalid lore line)' }
}) : []
}) ?? []
}
@@ -153,11 +150,7 @@ export class ResolvedItem extends ItemStack {
public getHoverName() {
const customName = this.get('custom_name', tag => tag.isString() ? tag.getAsString() : undefined)
if (customName) {
try {
return JSON.parse(customName)
} catch (e) {
return '(invalid custom name)'
}
return safeJsonParse(customName) ?? '(invalid custom name)'
}
const bookTitle = this.get('written_book_content', tag => tag.isCompound() ? (tag.hasCompound('title') ? tag.getCompound('title').getString('raw') : tag.getString('title')) : undefined)
@@ -167,11 +160,7 @@ export class ResolvedItem extends ItemStack {
const itemName = this.get('item_name', tag => tag.isString() ? tag.getAsString() : undefined)
if (itemName) {
try {
return JSON.parse(itemName)
} catch (e) {
return { text: '(invalid item name)' }
}
return safeJsonParse(itemName) ?? { text: '(invalid item name)' }
}
const guess = this.id.path

View File

@@ -3,7 +3,7 @@ import { BlockDefinition, BlockModel, Identifier, ItemRenderer, TextureAtlas, up
import config from '../Config.js'
import { message } from '../Utils.js'
import { fetchLanguage, fetchResources } from './DataFetcher.js'
import type { VersionId } from './Schemas.js'
import type { VersionId } from './Versions.js'
const Resources: Record<string, ResourceManager | Promise<ResourceManager>> = {}
@@ -44,7 +44,6 @@ export async function renderItem(version: VersionId, item: ItemStack) {
throw new Error('Cannot get WebGL2 context')
}
const renderer = new ItemRenderer(gl, item, resources)
console.log('Rendering', item.toString())
renderer.drawItem()
return canvas.toDataURL()
})()

View File

@@ -3,7 +3,7 @@ import { BlockDefinition, BlockModel, Identifier, ItemRenderer, TextureAtlas, up
import config from '../Config.js'
import { message } from '../Utils.js'
import { fetchLanguage, fetchResources } from './DataFetcher.js'
import type { VersionId } from './Schemas.js'
import type { VersionId } from './Versions.js'
const Resources: Record<string, ResourceManager | Promise<ResourceManager>> = {}
@@ -44,7 +44,6 @@ export async function renderItem(version: VersionId, item: ItemStack) {
throw new Error('Cannot get WebGL2 context')
}
const renderer = new ItemRenderer(gl, item, resources)
console.log('Rendering', item.toString())
renderer.drawItem()
return canvas.toDataURL()
})()

View File

@@ -1,147 +0,0 @@
import type { CollectionRegistry, INode, SchemaRegistry } from '@mcschema/core'
import { ChoiceNode, DataModel, Reference, StringNode } from '@mcschema/core'
import config from '../Config.js'
import { initPartners } from '../partners/index.js'
import { message } from '../Utils.js'
import { fetchData } from './DataFetcher.js'
export const VersionIds = ['1.15', '1.16', '1.17', '1.18', '1.18.2', '1.19', '1.19.3', '1.19.4', '1.20', '1.20.2', '1.20.3', '1.20.5', '1.21', '1.21.2', '1.21.4'] as const
export type VersionId = typeof VersionIds[number]
export const DEFAULT_VERSION: VersionId = '1.21.2'
export type BlockStateRegistry = {
[block: string]: {
properties?: {
[key: string]: string[],
},
default?: {
[key: string]: string,
},
},
}
type VersionData = {
collections: CollectionRegistry,
schemas: SchemaRegistry,
blockStates: BlockStateRegistry,
}
const Versions: Record<string, VersionData | Promise<VersionData>> = {}
type ModelData = {
model: DataModel,
version: VersionId,
}
const Models: Record<string, ModelData> = {}
const versionGetter: {
[versionId in VersionId]: () => Promise<{
getCollections: () => CollectionRegistry,
getSchemas: (collections: CollectionRegistry) => SchemaRegistry,
}>
} = {
'1.15': () => import('@mcschema/java-1.15'),
'1.16': () => import('@mcschema/java-1.16'),
'1.17': () => import('@mcschema/java-1.17'),
'1.18': () => import('@mcschema/java-1.18'),
'1.18.2': () => import('@mcschema/java-1.18.2'),
'1.19': () => import('@mcschema/java-1.19'),
'1.19.3': () => import('@mcschema/java-1.19.3'),
'1.19.4': () => import('@mcschema/java-1.19.4'),
'1.20': () => import('@mcschema/java-1.20'),
'1.20.2': () => import('@mcschema/java-1.20.2'),
'1.20.3': () => import('@mcschema/java-1.20.3'),
'1.20.5': () => import('@mcschema/java-1.20.5'),
'1.21': () => import('@mcschema/java-1.21'),
'1.21.2': () => import('@mcschema/java-1.21.2'),
'1.21.4': () => import('@mcschema/java-1.21.4'),
}
export let CachedDecorator: INode<any>
export let CachedFeature: INode<any>
export let CachedCollections: CollectionRegistry
export let CachedSchemas: SchemaRegistry
async function getVersion(id: VersionId): Promise<VersionData> {
if (!Versions[id]) {
Versions[id] = (async () => {
try {
const mcschema = await versionGetter[id]()
const collections = mcschema.getCollections()
const blockStates: BlockStateRegistry = {}
await fetchData(id, collections, blockStates)
const schemas = mcschema.getSchemas(collections)
initPartners(schemas, collections, id)
Versions[id] = { collections, schemas, blockStates }
return Versions[id]
} catch (e) {
throw new Error(`Cannot get version "${id}": ${message(e)}`)
}
})()
return Versions[id]
}
return Versions[id]
}
export async function getModel(version: VersionId, id: string): Promise<DataModel> {
if (!Models[id] || Models[id].version !== version) {
const versionData = await getVersion(version)
CachedDecorator = Reference(versionData.schemas, 'configured_decorator')
CachedFeature = ChoiceNode([
{
type: 'string',
node: StringNode(versionData.collections, { validator: 'resource', params: { pool: '$worldgen/configured_feature' } }),
},
{
type: 'object',
node: Reference(versionData.schemas, 'configured_feature'),
},
], { choiceContext: 'feature' })
const schemaName = config.generators.find(g => g.id === id)?.schema
if (!schemaName) {
throw new Error(`Cannot find model ${id}`)
}
try {
const schema = versionData.schemas.get(schemaName)
const model = new DataModel(schema, { wrapLists: true })
if (Models[id]) {
model.reset(Models[id].model.data, false)
} else {
model.validate(true)
model.history = [JSON.stringify(model.data)]
}
Models[id] = { model, version }
} catch (e) {
const err = new Error(`Cannot get generator "${id}" for version "${version}": ${message(e)}`)
if (e instanceof Error) err.stack = e.stack
throw err
}
}
return Models[id].model
}
export async function getCollections(version: VersionId): Promise<CollectionRegistry> {
const versionData = await getVersion(version)
CachedCollections = versionData.collections
return versionData.collections
}
export async function getBlockStates(version: VersionId): Promise<BlockStateRegistry> {
const versionData = await getVersion(version)
return versionData.blockStates
}
export async function getSchemas(version: VersionId): Promise<SchemaRegistry> {
const versionData = await getVersion(version)
CachedSchemas = versionData.schemas
return versionData.schemas
}
export function checkVersion(versionId: string, minVersionId: string | undefined, maxVersionId?: string) {
const version = config.versions.findIndex(v => v.id === versionId)
const minVersion = minVersionId ? config.versions.findIndex(v => v.id === minVersionId) : 0
const maxVersion = maxVersionId ? config.versions.findIndex(v => v.id === maxVersionId) : config.versions.length - 1
return minVersion <= version && version <= maxVersion
}

View File

@@ -1,14 +1,13 @@
import lz from 'lz-string'
import type { VersionId } from './Schemas.js'
import type { VersionId } from './Versions.js'
const API_PREFIX = 'https://snippets.misode.workers.dev'
const ShareCache = new Map<string, string>()
export async function shareSnippet(type: string, version: VersionId, jsonData: any, show_preview: boolean) {
export async function shareSnippet(type: string, version: VersionId, text: string, show_preview: boolean) {
try {
const raw = JSON.stringify(jsonData)
const data = lz.compressToBase64(raw)
const data = lz.compressToBase64(text)
const body = JSON.stringify({ data, type, version, show_preview })
let id = ShareCache.get(body)
if (!id) {
@@ -16,7 +15,7 @@ export async function shareSnippet(type: string, version: VersionId, jsonData: a
ShareCache.set(body, snippet.id)
id = snippet.id as string
}
return { id, length: raw.length, compressed: data.length, rate: raw.length / data.length }
return { id, length: text.length, compressed: data.length, rate: text.length / data.length }
} catch (e) {
if (e instanceof Error) {
e.message = `Error creating share link: ${e.message}`
@@ -30,7 +29,7 @@ export async function getSnippet(id: string) {
const snippet = await fetchApi(`/${id}`)
return {
...snippet,
data: JSON.parse(lz.decompressFromBase64(snippet.data) ?? '{}'),
text: lz.decompressFromBase64(snippet.data) ?? '{}',
}
} catch (e) {
if (e instanceof Error) {

View File

@@ -1,7 +1,7 @@
import { NbtTag } from 'deepslate'
import yaml from 'js-yaml'
import { Store } from '../Store.js'
import { jsonToNbt } from '../Utils.js'
import { jsonToNbt, safeJsonParse } from '../Utils.js'
const INDENTS: Record<string, number | string | undefined> = {
'2_spaces': 2,
@@ -10,28 +10,18 @@ const INDENTS: Record<string, number | string | undefined> = {
minified: undefined,
}
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
let commentJson: typeof import('comment-json') | null = null
const FORMATS: Record<string, {
parse: (v: string) => Promise<unknown>,
stringify: (v: unknown, indentation: string | number | undefined) => string,
parse: (source: string) => string,
stringify: (source: string, indent: string | number | undefined) => string,
}> = {
json: {
parse: async (v) => {
try {
return JSON.parse(v)
} catch (e) {
commentJson = await import('comment-json')
return commentJson.parse(v)
}
},
stringify: (v, i) => (commentJson ?? JSON).stringify(v, null, i) + '\n',
parse: (s) => s,
stringify: (s) => s,
},
snbt: {
parse: async (v) => NbtTag.fromString(v).toSimplifiedJson(),
stringify: (v, i) => {
const tag = jsonToNbt(v)
parse: (s) => JSON.stringify(NbtTag.fromString(s).toSimplifiedJson(), null, 2),
stringify: (s, i) => {
const tag = jsonToNbt(safeJsonParse(s) ?? {})
if (i === undefined) {
return tag.toString()
}
@@ -39,20 +29,20 @@ const FORMATS: Record<string, {
},
},
yaml: {
parse: async (v) => yaml.load(v),
stringify: (v, i) => yaml.dump(v, {
parse: (s) => JSON.stringify(yaml.load(s), null, 2),
stringify: (s, i) => yaml.dump(safeJsonParse(s) ?? {}, {
flowLevel: i === undefined ? 0 : -1,
indent: typeof i === 'string' ? 4 : i,
}),
},
}
export function stringifySource(data: unknown, format?: string, indent?: string) {
return FORMATS[format ?? Store.getFormat()].stringify(data, INDENTS[indent ?? Store.getIndent()])
export function stringifySource(source: string, format?: string, indent?: string) {
return FORMATS[format ?? Store.getFormat()].stringify(source, INDENTS[indent ?? Store.getIndent()])
}
export async function parseSource(data: string, format: string) {
return await FORMATS[format].parse(data)
export async function parseSource(source: string, format: string) {
return FORMATS[format].parse(source)
}
export function getSourceIndent(indent: string) {

View File

@@ -0,0 +1,478 @@
import * as core from '@spyglassmc/core'
import { BrowserExternals } from '@spyglassmc/core/lib/browser.js'
import * as je from '@spyglassmc/java-edition'
import { ReleaseVersion } from '@spyglassmc/java-edition/lib/dependency/index.js'
import * as json from '@spyglassmc/json'
import { localize } from '@spyglassmc/locales'
import * as mcdoc from '@spyglassmc/mcdoc'
import * as nbt from '@spyglassmc/nbt'
import * as zip from '@zip.js/zip.js'
import sparkmd5 from 'spark-md5'
import { TextDocument } from 'vscode-languageserver-textdocument'
import type { ConfigGenerator } from '../Config.js'
import siteConfig from '../Config.js'
import { computeIfAbsent, genPath } from '../Utils.js'
import type { VersionMeta } from './DataFetcher.js'
import { fetchBlockStates, fetchRegistries, fetchVanillaMcdoc, fetchVersions, getVersionChecksum } from './DataFetcher.js'
import { IndexedDbFileSystem } from './FileSystem.js'
import type { VersionId } from './Versions.js'
export const CACHE_URI = 'file:///cache/'
export const ROOT_URI = 'file:///root/'
export const DEPENDENCY_URI = `${ROOT_URI}dependency/`
export const UNSAVED_URI = `${ROOT_URI}unsaved/`
export const PROJECTS_URI = `${ROOT_URI}projects/`
export const DRAFTS_URI = `${ROOT_URI}drafts/`
const INITIAL_DIRS = [CACHE_URI, ROOT_URI, DEPENDENCY_URI, UNSAVED_URI, PROJECTS_URI, DRAFTS_URI]
const builtinMcdoc = `
use ::java::server::util::text::Text
use ::java::data::worldgen::dimension::Dimension
dispatch minecraft:resource[text_component] to Text
dispatch minecraft:resource[world] to struct WorldSettings {
seed: #[random] long,
/// Defaults to \`true\`.
generate_features?: boolean,
/// Defaults to \`false\`.
bonus_chest?: boolean,
legacy_custom_options?: string,
dimensions: struct {
[#[id="dimension"] string]: Dimension,
},
}
`
interface ClientDocument {
doc: TextDocument
undoStack: string[]
redoStack: string[]
}
export class SpyglassClient {
public static readonly FS = new IndexedDbFileSystem()
public readonly fs = SpyglassClient.FS
public readonly externals: core.Externals = {
...BrowserExternals,
archive: {
...BrowserExternals.archive,
decompressBall,
},
crypto: {
// Swap the web crypto sha1 for an md5 implementation, which is about twice as fast
getSha1: async (data: string | Uint8Array) => {
if (typeof data === 'string') {
data = new TextEncoder().encode(data)
}
return sparkmd5.ArrayBuffer.hash(data)
},
},
fs: SpyglassClient.FS,
}
public readonly documents = new Map<string, ClientDocument>()
public async createService(version: VersionId) {
return SpyglassService.create(version, this)
}
}
export class SpyglassService {
private static activeServiceId = 1
private readonly fileWatchers = new Map<string, ((docAndNode: core.DocAndNode) => void)[]>()
private readonly treeWatchers: { prefix: string, handler: (uris: string[]) => void }[] = []
private constructor (
public readonly version: VersionId,
private readonly service: core.Service,
private readonly client: SpyglassClient,
) {
service.project.on('documentUpdated', (e) => {
const uriWatchers = this.fileWatchers.get(e.doc.uri) ?? []
for (const handler of uriWatchers) {
handler(e)
}
})
let treeWatcherTask = Promise.resolve()
let hasPendingTask = false
const treeWatcher = () => {
hasPendingTask = true
// Wait for previous task to finish, then re-run once after 5 ms
treeWatcherTask = treeWatcherTask.finally(async () => {
if (!hasPendingTask) {
return
}
hasPendingTask = false
await new Promise((res) => setTimeout(res, 5))
await Promise.all(this.treeWatchers.map(async ({ prefix, handler }) => {
const entries = await client.fs.readdir(prefix)
handler(entries.flatMap(e => {
return e.isFile() ? [e.name.slice(prefix.length)] : []
}))
}))
})
}
service.project.on('fileCreated', treeWatcher)
service.project.on('fileDeleted', treeWatcher)
}
public getCheckerContext(doc?: TextDocument, errors?: core.LanguageError[]) {
if (!doc) {
doc = TextDocument.create('file:///unknown.json', 'json', 1, '')
}
const err = new core.ErrorReporter()
if (errors) {
err.errors = errors
}
return core.CheckerContext.create(this.service.project, { doc, err })
}
public dissectUri(uri: string) {
return je.binder.dissectUri(uri, this.getCheckerContext(TextDocument.create(uri, 'json', 1, '')))
}
public async openFile(uri: string) {
const lang = core.fileUtil.extname(uri)?.slice(1) ?? 'txt'
const content = await this.readFile(uri)
if (!content) {
return undefined
}
await this.service.project.onDidOpen(uri, lang, 1, content)
const docAndNode = await this.service.project.ensureClientManagedChecked(uri)
if (!docAndNode) {
return undefined
}
const document = this.client.documents.get(uri)
if (document === undefined) {
this.client.documents.set(uri, { doc: docAndNode.doc, undoStack: [], redoStack: [] })
}
return docAndNode
}
public async readFile(uri: string): Promise<string | undefined> {
try {
const buffer = await this.service.project.externals.fs.readFile(uri)
return new TextDecoder().decode(buffer)
} catch (e) {
return undefined
}
}
private async notifyChange(doc: TextDocument) {
const docAndNode = this.service.project.getClientManaged(doc.uri)
if (docAndNode) {
await this.service.project.onDidChange(doc.uri, [{ text: doc.getText() }], doc.version + 1)
} else {
await this.service.project.onDidOpen(doc.uri, doc.languageId, doc.version, doc.getText())
}
await this.service.project.ensureClientManagedChecked(doc.uri)
}
public async writeFile(uri: string, content: string) {
const document = this.client.documents.get(uri)
if (document) {
document.undoStack.push(document.doc.getText())
document.redoStack = []
TextDocument.update(document.doc, [{ text: content }], document.doc.version + 1)
}
await this.service.project.externals.fs.writeFile(uri, content)
if (document) {
await this.notifyChange(document.doc)
}
}
public async renameFile(oldUri: string, newUri: string) {
const content = await this.readFile(oldUri)
if (!content) {
throw new Error(`Cannot rename nonexistent file ${oldUri}`)
}
await this.service.project.externals.fs.writeFile(newUri, content)
await this.service.project.externals.fs.unlink(oldUri)
const d = this.client.documents.get(oldUri)
if (d) {
const doc = TextDocument.create(newUri, d.doc.languageId, d.doc.version, d.doc.getText())
this.client.documents.set(newUri, { ...d, doc })
this.client.documents.delete(oldUri)
}
}
public async applyEdit(uri: string, edit: (node: core.FileNode<core.AstNode>) => void) {
const document = this.client.documents.get(uri)
if (document !== undefined) {
document.undoStack.push(document.doc.getText())
document.redoStack = []
const docAndNode = this.service.project.getClientManaged(uri)
if (!docAndNode) {
throw new Error(`[Spyglass#applyEdit] Cannot get doc and node: ${uri}`)
}
edit(docAndNode.node)
const newText = this.service.format(docAndNode.node, docAndNode.doc, 2, true)
TextDocument.update(document.doc, [{ text: newText }], document.doc.version + 1)
await this.service.project.externals.fs.writeFile(uri, document.doc.getText())
await this.notifyChange(document.doc)
}
}
public formatNode(node: json.JsonNode, uri: string) {
const formatter = this.service.project.meta.getFormatter(node.type)
const doc = TextDocument.create(uri, 'json', 1, '')
const ctx = core.FormatterContext.create(this.service.project, { doc, tabSize: 2, insertSpaces: true })
return formatter(node, ctx)
}
public async undoEdit(uri: string) {
const document = this.client.documents.get(uri)
if (document === undefined) {
throw new Error(`[Spyglass#undoEdits] Document doesn't exist: ${uri}`)
}
const lastUndo = document.undoStack.pop()
if (lastUndo === undefined) {
return
}
document.redoStack.push(document.doc.getText())
TextDocument.update(document.doc, [{ text: lastUndo }], document.doc.version + 1)
await this.service.project.externals.fs.writeFile(uri, document.doc.getText())
await this.notifyChange(document.doc)
}
public async redoEdit(uri: string) {
const document = this.client.documents.get(uri)
if (document === undefined) {
throw new Error(`[Spyglass#redoEdits] Document doesn't exist: ${uri}`)
}
const lastRedo = document.redoStack.pop()
if (lastRedo === undefined) {
return
}
document.undoStack.push(document.doc.getText())
TextDocument.update(document.doc, [{ text: lastRedo }], document.doc.version + 1)
await this.service.project.externals.fs.writeFile(uri, document.doc.getText())
await this.notifyChange(document.doc)
}
public getUnsavedFileUri(gen: ConfigGenerator) {
if (gen.id === 'pack_mcmeta') {
return `${UNSAVED_URI}pack.mcmeta`
}
const pack = gen.tags?.includes('assets') ? 'assets' : 'data'
return `${UNSAVED_URI}${pack}/draft/${genPath(gen, this.version)}/draft.json`
}
public watchFile(uri: string, handler: (docAndNode: core.DocAndNode) => void) {
const uriWatchers = computeIfAbsent(this.fileWatchers, uri, () => [])
uriWatchers.push(handler)
}
public unwatchFile(uri: string, handler: (docAndNode: core.DocAndNode) => void) {
const uriWatchers = computeIfAbsent(this.fileWatchers, uri, () => [])
const index = uriWatchers.findIndex(w => w === handler)
uriWatchers.splice(index, 1)
}
public watchTree(prefix: string, handler: (uris: string[]) => void) {
this.treeWatchers.push({ prefix, handler })
}
public unwatchTree(prefix: string, handler: (uris: string[]) => void) {
const index = this.treeWatchers.findIndex(w => w.prefix === prefix && w.handler === handler)
this.treeWatchers.splice(index, 1)
}
public static async create(versionId: VersionId, client: SpyglassClient) {
SpyglassService.activeServiceId += 1
const currentServiceId = SpyglassService.activeServiceId
await Promise.allSettled(INITIAL_DIRS.map(async uri => client.externals.fs.mkdir(uri)))
const version = siteConfig.versions.find(v => v.id === versionId)!
const logger = console
const service = new core.Service({
logger,
profilers: new core.ProfilerFactory(logger, [
'cache#load',
'cache#save',
'project#init',
'project#ready',
'project#ready#bind',
]),
project: {
cacheRoot: CACHE_URI,
projectRoots: [ROOT_URI],
externals: client.externals,
defaultConfig: core.ConfigService.merge(core.VanillaConfig, {
env: {
gameVersion: version.dynamic ? version.id : version.ref,
dependencies: ['@vanilla-mcdoc', '@misode-mcdoc'],
customResources: {
text_component: {
category: 'text_component',
},
world: {
category: 'world',
},
// Partner resources
...Object.fromEntries(siteConfig.generators.filter(gen => gen.dependency).map(gen =>
[gen.path ?? gen.id, {
category: gen.id,
}]
)),
},
},
lint: {
idOmitDefaultNamespace: false,
undeclaredSymbol: [
{
if: { category: ['bossbar', 'objective', 'team', 'shader'] },
then: { declare: 'block' },
},
...core.VanillaConfig.lint.undeclaredSymbol as any[],
],
},
}),
initializers: [mcdoc.initialize, initialize],
},
})
await service.project.ready()
setTimeout(() => {
if (currentServiceId === SpyglassService.activeServiceId) {
service.project.cacheService.save()
} else {
logger.info('[SpyglassService] Skipped saving the cache because another service is active')
}
}, 10_000)
return new SpyglassService(versionId, service, client)
}
}
async function decompressBall(buffer: Uint8Array, options?: { stripLevel?: number }): Promise<core.DecompressedFile[]> {
const reader = new zip.ZipReader(new zip.BlobReader(new Blob([buffer])))
const entries = await reader.getEntries()
return await Promise.all(entries.map(async e => {
const data = await e.getData?.(new zip.Uint8ArrayWriter())
const path = options?.stripLevel === 1 ? e.filename.substring(e.filename.indexOf('/') + 1) : e.filename
const type = e.directory ? 'dir' : 'file'
return { data, path, mtime: '', type, mode: 0 }
}))
}
async function compressBall(files: [string, string][]): Promise<Uint8Array> {
const writer = new zip.ZipWriter(new zip.Uint8ArrayWriter())
await Promise.all(files.map(async ([name, data]) => {
await writer.add(name, new zip.TextReader(data))
}))
return await writer.close()
}
const initialize: core.ProjectInitializer = async (ctx) => {
const { config, logger, meta, externals, cacheRoot } = ctx
meta.registerDependencyProvider('@vanilla-mcdoc', async () => {
const uri: string = new core.Uri('downloads/vanilla-mcdoc.tar.gz', cacheRoot).toString()
const buffer = await fetchVanillaMcdoc()
await core.fileUtil.writeFile(externals, uri, new Uint8Array(buffer))
return { info: { startDepth: 1 }, uri }
})
meta.registerDependencyProvider('@misode-mcdoc', async () => {
const uri: string = new core.Uri('downloads/misode-mcdoc.tar.gz', cacheRoot).toString()
const buffer = await compressBall([['builtin.mcdoc', builtinMcdoc]])
await core.fileUtil.writeFile(externals, uri, buffer)
return { uri }
})
meta.registerUriBinder(je.binder.uriBinder)
const versions = await fetchVersions()
const release = config.env.gameVersion as ReleaseVersion
const version = siteConfig.versions.find(v => {
return v.ref ? v.ref === release : v.id === release
})
if (version === undefined) {
logger.error(`[initialize] Failed finding game version matching ${release}.`)
return
}
const summary: je.dependency.McmetaSummary = {
registries: Object.fromEntries((await fetchRegistries(version.id)).entries()),
blocks: Object.fromEntries([...(await fetchBlockStates(version.id)).entries()]
.map(([id, data]) => [id, data])),
fluids: je.dependency.Fluids,
commands: { type: 'root', children: {} },
}
const versionChecksum = getVersionChecksum(version.id)
meta.registerSymbolRegistrar('mcmeta-summary', {
checksum: versionChecksum,
registrar: je.dependency.symbolRegistrar(summary, release),
})
registerAttributes(meta, release, versions)
json.initialize(ctx)
je.json.initialize(ctx)
je.mcf.initialize(ctx, summary.commands, release)
nbt.initialize(ctx)
return { loadedVersion: release }
}
// Duplicate these from spyglass for now, until they are exported separately
function registerAttributes(meta: core.MetaRegistry, release: ReleaseVersion, versions: VersionMeta[]) {
mcdoc.runtime.registerAttribute(meta, 'since', mcdoc.runtime.attribute.validator.string, {
filterElement: (config, ctx) => {
if (!config.startsWith('1.')) {
ctx.logger.warn(`Invalid mcdoc attribute for "since": ${config}`)
return true
}
return ReleaseVersion.cmp(release, config as ReleaseVersion) >= 0
},
})
mcdoc.runtime.registerAttribute(meta, 'until', mcdoc.runtime.attribute.validator.string, {
filterElement: (config, ctx) => {
if (!config.startsWith('1.')) {
ctx.logger.warn(`Invalid mcdoc attribute for "until": ${config}`)
return true
}
return ReleaseVersion.cmp(release, config as ReleaseVersion) < 0
},
})
mcdoc.runtime.registerAttribute(
meta,
'deprecated',
mcdoc.runtime.attribute.validator.optional(mcdoc.runtime.attribute.validator.string),
{
mapField: (config, field, ctx) => {
if (config === undefined) {
return { ...field, deprecated: true }
}
if (!config.startsWith('1.')) {
ctx.logger.warn(`Invalid mcdoc attribute for "deprecated": ${config}`)
return field
}
if (ReleaseVersion.cmp(release, config as ReleaseVersion) >= 0) {
return { ...field, deprecated: true }
}
return field
},
},
)
const maxPackFormat = versions[0].data_pack_version
mcdoc.runtime.registerAttribute(meta, 'pack_format', () => undefined, {
checker: (_, typeDef) => {
if (typeDef.kind !== 'literal' || typeof typeDef.value.value !== 'number') {
return undefined
}
const target = typeDef.value.value
return (node, ctx) => {
if (target > maxPackFormat) {
ctx.err.report(
localize('expected', localize('mcdoc.runtime.checker.range.number', localize('mcdoc.runtime.checker.range.right-inclusive', maxPackFormat))),
node,
3,
)
}
}
},
})
}

View File

@@ -0,0 +1,13 @@
import config from '../Config.js'
export const VersionIds = ['1.15', '1.16', '1.17', '1.18', '1.18.2', '1.19', '1.19.3', '1.19.4', '1.20', '1.20.2', '1.20.3', '1.20.5', '1.21', '1.21.2', '1.21.4'] as const
export type VersionId = typeof VersionIds[number]
export const DEFAULT_VERSION: VersionId = '1.21.2'
export function checkVersion(versionId: string, minVersionId: string | undefined, maxVersionId?: string) {
const version = config.versions.findIndex(v => v.id === versionId)
const minVersion = minVersionId ? config.versions.findIndex(v => v.id === minVersionId) : 0
const maxVersion = maxVersionId ? config.versions.findIndex(v => v.id === maxVersionId) : config.versions.length - 1
return minVersion <= version && version <= maxVersion
}

View File

@@ -1,5 +1,5 @@
export * from './Article.js'
export * from './DataFetcher.js'
export * from './Schemas.js'
export * from './Sharing.js'
export * from './Source.js'
export * from './Versions.js'

View File

@@ -177,129 +177,109 @@
{
"id": "loot_table",
"url": "loot-table",
"schema": "loot_table",
"wiki": "https://minecraft.wiki/w/Loot_table"
},
{
"id": "predicate",
"url": "predicate",
"schema": "predicate",
"wiki": "https://minecraft.wiki/w/Predicate"
},
{
"id": "item_modifier",
"url": "item-modifier",
"schema": "item_modifier",
"minVersion": "1.17",
"wiki": "https://minecraft.wiki/w/Item_modifier"
},
{
"id": "advancement",
"url": "advancement",
"schema": "advancement",
"wiki": "https://minecraft.wiki/w/Custom_advancement"
},
{
"id": "recipe",
"url": "recipe",
"schema": "recipe",
"wiki": "https://minecraft.wiki/w/Recipe#JSON_format"
},
{
"id": "chat_type",
"url": "chat-type",
"schema": "chat_type",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Chat_type"
},
{
"id": "damage_type",
"url": "damage-type",
"schema": "damage_type",
"minVersion": "1.19.4",
"wiki": "https://minecraft.wiki/w/Damage_type"
},
{
"id": "trim_material",
"url": "trim-material",
"schema": "trim_material",
"minVersion": "1.19.4"
},
{
"id": "trim_pattern",
"url": "trim-pattern",
"schema": "trim_pattern",
"minVersion": "1.19.4"
},
{
"id": "banner_pattern",
"url": "banner-pattern",
"schema": "banner_pattern",
"minVersion": "1.20.5"
},
{
"id": "wolf_variant",
"url": "wolf-variant",
"schema": "wolf_variant",
"minVersion": "1.20.5"
},
{
"id": "enchantment",
"url": "enchantment",
"schema": "enchantment",
"minVersion": "1.21",
"wiki": "https://minecraft.wiki/w/Custom_enchantment"
},
{
"id": "enchantment_provider",
"url": "enchantment-provider",
"schema": "enchantment_provider",
"minVersion": "1.21",
"wiki": "https://minecraft.wiki/w/Enchantment_provider"
},
{
"id": "painting_variant",
"url": "painting-variant",
"schema": "painting_variant",
"minVersion": "1.21",
"wiki": "https://minecraft.wiki/w/Painting_variant"
},
{
"id": "jukebox_song",
"url": "jukebox-song",
"schema": "jukebox_song",
"minVersion": "1.21",
"wiki": "https://minecraft.wiki/w/Jukebox_song_definition"
},
{
"id": "instrument",
"url": "instrument",
"schema": "instrument",
"minVersion": "1.21.2"
},
{
"id": "trial_spawner",
"url": "trial-spawner",
"schema": "trial_spawner",
"minVersion": "1.21.2"
},
{
"id": "text_component",
"url": "text-component",
"schema": "text_component",
"noPath": true,
"wiki": "https://minecraft.wiki/w/Raw_JSON_text_format#Java_Edition"
},
{
"id": "pack_mcmeta",
"url": "pack-mcmeta",
"schema": "pack_mcmeta",
"wiki": "https://minecraft.wiki/w/Data_pack#pack.mcmeta"
},
{
"id": "dimension",
"url": "dimension",
"schema": "dimension",
"tags": ["worldgen"],
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Custom_dimension"
@@ -307,7 +287,6 @@
{
"id": "dimension_type",
"url": "dimension-type",
"schema": "dimension_type",
"tags": ["worldgen"],
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Dimension_type"
@@ -316,7 +295,6 @@
"id": "worldgen/biome",
"url": "worldgen/biome",
"tags": ["worldgen"],
"schema": "biome",
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Custom_biome"
},
@@ -324,7 +302,6 @@
"id": "worldgen/configured_carver",
"url": "worldgen/carver",
"tags": ["worldgen"],
"schema": "configured_carver",
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Custom_carver"
},
@@ -332,7 +309,6 @@
"id": "worldgen/configured_feature",
"url": "worldgen/feature",
"tags": ["worldgen"],
"schema": "configured_feature",
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Configured_feature"
},
@@ -340,7 +316,6 @@
"id": "worldgen/placed_feature",
"url": "worldgen/placed-feature",
"tags": ["worldgen"],
"schema": "placed_feature",
"minVersion": "1.18",
"wiki": "https://minecraft.wiki/w/Placed_feature"
},
@@ -348,7 +323,6 @@
"id": "worldgen/density_function",
"url": "worldgen/density-function",
"tags": ["worldgen"],
"schema": "density_function",
"minVersion": "1.18.2",
"wiki": "https://minecraft.wiki/w/Density_function"
},
@@ -356,7 +330,6 @@
"id": "worldgen/noise",
"url": "worldgen/noise",
"tags": ["worldgen"],
"schema": "noise_parameters",
"minVersion": "1.18",
"wiki": "https://minecraft.wiki/w/Noise"
},
@@ -364,7 +337,6 @@
"id": "worldgen/noise_settings",
"url": "worldgen/noise-settings",
"tags": ["worldgen"],
"schema": "noise_settings",
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Custom_noise_settings"
},
@@ -372,7 +344,6 @@
"id": "worldgen/configured_structure_feature",
"url": "worldgen/structure-feature",
"tags": ["worldgen"],
"schema": "configured_structure_feature",
"minVersion": "1.16",
"maxVersion": "1.18.2"
},
@@ -380,7 +351,6 @@
"id": "worldgen/structure",
"url": "worldgen/structure",
"tags": ["worldgen"],
"schema": "structure",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Custom_structure"
},
@@ -388,7 +358,6 @@
"id": "worldgen/structure_set",
"url": "worldgen/structure-set",
"tags": ["worldgen"],
"schema": "structure_set",
"minVersion": "1.18.2",
"wiki": "https://minecraft.wiki/w/Structure_set"
},
@@ -396,7 +365,6 @@
"id": "worldgen/configured_surface_builder",
"url": "worldgen/surface-builder",
"tags": ["worldgen"],
"schema": "configured_surface_builder",
"minVersion": "1.16",
"maxVersion": "1.17",
"wiki": "https://minecraft.wiki/w/Configured_surface_builder"
@@ -405,7 +373,6 @@
"id": "worldgen/processor_list",
"url": "worldgen/processor-list",
"tags": ["worldgen"],
"schema": "processor_list",
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Processor_list"
},
@@ -413,7 +380,6 @@
"id": "worldgen/template_pool",
"url": "worldgen/template-pool",
"tags": ["worldgen"],
"schema": "template_pool",
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Template_pool"
},
@@ -421,7 +387,6 @@
"id": "worldgen/world_preset",
"url": "worldgen/world-preset",
"tags": ["worldgen"],
"schema": "world_preset",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Custom_world_preset"
},
@@ -429,14 +394,12 @@
"id": "worldgen/flat_level_generator_preset",
"url": "worldgen/flat-world-preset",
"tags": ["worldgen"],
"schema": "flat_level_generator_preset",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Custom_world_preset#Superflat_Level_Generation_Preset"
},
{
"id": "world",
"url": "world",
"schema": "world_settings",
"noPath": true,
"tags": ["worldgen"],
"minVersion": "1.16",
@@ -448,7 +411,6 @@
"url": "tags/block",
"tags": ["tags"],
"path": "tags/block",
"schema": "block_tag",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
{
@@ -456,7 +418,6 @@
"url": "tags/enchantment",
"tags": ["tags"],
"path": "tags/enchantment",
"schema": "enchantment_tag",
"minVersion": "1.20.5",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -465,7 +426,6 @@
"url": "tags/entity-type",
"tags": ["tags"],
"path": "tags/entity_type",
"schema": "entity_type_tag",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
{
@@ -473,7 +433,6 @@
"url": "tags/fluid",
"tags": ["tags"],
"path": "tags/fluid",
"schema": "fluid_tag",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
{
@@ -481,7 +440,6 @@
"url": "tags/game-event",
"tags": ["tags"],
"path": "tags/game_event",
"schema": "game_event_tag",
"minVersion": "1.17",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -490,7 +448,6 @@
"url": "tags/item",
"tags": ["tags"],
"path": "tags/item",
"schema": "item_tag",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
{
@@ -498,7 +455,6 @@
"url": "tags/damage-type",
"tags": ["tags"],
"path": "tags/damage_type",
"schema": "damage_type_tag",
"minVersion": "1.19.4",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -507,7 +463,6 @@
"url": "tags/biome",
"tags": ["tags", "worldgen"],
"path": "tags/worldgen/biome",
"schema": "biome_tag",
"minVersion": "1.18.2",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -516,7 +471,6 @@
"url": "tags/structure",
"tags": ["tags", "worldgen"],
"path": "tags/worldgen/structure",
"schema": "structure_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -525,7 +479,6 @@
"url": "tags/structure-set",
"tags": ["tags", "worldgen"],
"path": "tags/worldgen/structure_set",
"schema": "structure_set_tag",
"minVersion": "1.18.2",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -534,7 +487,6 @@
"url": "tags/flat-world-preset",
"tags": ["tags", "worldgen"],
"path": "tags/worldgen/flat_level_generator_preset",
"schema": "flat_level_generator_preset_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -543,7 +495,6 @@
"url": "tags/world-preset",
"tags": ["tags", "worldgen"],
"path": "tags/worldgen/world_preset",
"schema": "world_preset_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -552,7 +503,6 @@
"url": "tags/banner-pattern",
"tags": ["tags"],
"path": "tags/banner_pattern",
"schema": "banner_pattern_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -561,7 +511,6 @@
"url": "tags/cat-variant",
"tags": ["tags"],
"path": "tags/cat_variant",
"schema": "cat_variant_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -570,7 +519,6 @@
"url": "tags/enchantment",
"tags": ["tags"],
"path": "tags/enchantment",
"schema": "enchantment_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -579,7 +527,6 @@
"url": "tags/instrument",
"tags": ["tags"],
"path": "tags/instrument",
"schema": "instrument_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -588,7 +535,6 @@
"url": "tags/painting-variant",
"tags": ["tags"],
"path": "tags/painting_variant",
"schema": "painting_variant_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -597,7 +543,6 @@
"url": "tags/point-of-interest-type",
"tags": ["tags"],
"path": "tags/point_of_interest_type",
"schema": "point_of_interest_type_tag",
"minVersion": "1.19",
"wiki": "https://minecraft.wiki/w/Tag#Java_Edition"
},
@@ -606,7 +551,6 @@
"url": "assets/blockstate",
"path": "blockstates",
"tags": ["assets"],
"schema": "block_definition",
"wiki": "https://minecraft.wiki/w/Tutorials/Models#Block_states"
},
{
@@ -622,15 +566,20 @@
"url": "assets/model",
"path": "models",
"tags": ["assets"],
"schema": "model",
"wiki": "https://minecraft.wiki/w/Tutorials/Models"
},
{
"id": "lang",
"url": "assets/lang",
"path": "lang",
"tags": ["assets"],
"wiki": "https://minecraft.wiki/w/Resource_pack#Language"
},
{
"id": "font",
"url": "assets/font",
"path": "font",
"tags": ["assets"],
"schema": "font",
"minVersion": "1.16",
"wiki": "https://minecraft.wiki/w/Resource_pack#Fonts"
},
@@ -639,137 +588,121 @@
"url": "assets/atlas",
"path": "atlases",
"tags": ["assets"],
"schema": "atlas",
"minVersion": "1.19.3",
"wiki": "https://minecraft.wiki/w/Resource_pack#Atlases"
},
{
"id": "immersive_weathering.block_growth",
"id": "post_effect",
"url": "assets/post-effect",
"path": "post_effect",
"tags": ["assets"],
"minVersion": "1.21.2",
"wiki": "https://minecraft.wiki/w/Shader#Post-processing_effects"
},
{
"id": "immersive_weathering:block_growth",
"url": "immersive-weathering/block-growth",
"path": "block_growths",
"tags": ["partners"],
"schema": "immersive_weathering:block_growth",
"dependency": "immersive_weathering",
"minVersion": "1.18.2"
},
{
"id": "lithostitched.worldgen_modifier",
"url": "lithostitched/worldgen-modifier",
"path": "lithostitched/worldgen_modifier",
"tags": ["partners"],
"schema": "lithostitched:worldgen_modifier",
"minVersion": "1.20.2",
"wiki": "https://github.com/Apollounknowndev/lithostitched/wiki/Worldgen-Modifiers"
},
{
"id": "neoforge.biome_modifier",
"id": "neoforge:biome_modifier",
"url": "neoforge/biome-modifier",
"path": "neoforge/biome_modifier",
"tags": ["partners"],
"schema": "neoforge:biome_modifier",
"dependency": "neoforge",
"minVersion": "1.20.2",
"wiki": "https://docs.neoforged.net/docs/worldgen/biomemodifier"
},
{
"id": "neoforge.data_map_compostables",
"id": "neoforge:data_map_compostables",
"url": "neoforge/data-map-compostables",
"path": "neoforge/data_map_compostables",
"tags": ["partners"],
"schema": "neoforge:data_map_compostables",
"dependency": "neoforge",
"minVersion": "1.20.4",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgecompostables"
},
{
"id": "neoforge.data_map_furnace_fuels",
"id": "neoforge:data_map_furnace_fuels",
"url": "neoforge/data-map-furnace-fuels",
"path": "neoforge/data_map_furnace_fuels",
"tags": ["partners"],
"schema": "neoforge:data_map_furnace_fuels",
"dependency": "neoforge",
"minVersion": "1.20.4",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgefurnace_fuels"
},
{
"id": "neoforge.data_map_monster_room_mobs",
"id": "neoforge:data_map_monster_room_mobs",
"url": "neoforge/data-map-monster-room-mobs",
"path": "neoforge/data_map_monster_room_mobs",
"tags": ["partners"],
"schema": "neoforge:data_map_monster_room_mobs",
"dependency": "neoforge",
"minVersion": "1.20.6",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgemonster_room_mobs"
},
{
"id": "neoforge.data_map_oxidizables",
"id": "neoforge:data_map_oxidizables",
"url": "neoforge/data-map-oxidizables",
"path": "neoforge/data_map_oxidizables",
"tags": ["partners"],
"schema": "neoforge:data_map_oxidizables",
"dependency": "neoforge",
"minVersion": "1.21",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgeoxidizables"
},
{
"id": "neoforge.data_map_parrot_imitations",
"id": "neoforge:data_map_parrot_imitations",
"url": "neoforge/data-map-parrot-imitations",
"path": "neoforge/data_map_parrot_imitations",
"tags": ["partners"],
"schema": "neoforge:data_map_parrot_imitations",
"dependency": "neoforge",
"minVersion": "1.20.4",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgeparrot_imitations"
},
{
"id": "neoforge.data_map_raid_hero_gifts",
"id": "neoforge:data_map_raid_hero_gifts",
"url": "neoforge/data-map-raid-hero-gifts",
"path": "neoforge/data_map_raid_hero_gifts",
"tags": ["partners"],
"schema": "neoforge:data_map_raid_hero_gifts",
"dependency": "neoforge",
"minVersion": "1.20.4",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgeraid_hero_gifts"
},
{
"id": "neoforge.data_map_vibration_frequencies",
"id": "neoforge:data_map_vibration_frequencies",
"url": "neoforge/data-map-vibration-frequencies",
"path": "neoforge/data_map_vibration_frequencies",
"tags": ["partners"],
"schema": "neoforge:data_map_vibration_frequencies",
"dependency": "neoforge",
"minVersion": "1.20.4",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgevibration_frequencies"
},
{
"id": "neoforge.data_map_waxables",
"id": "neoforge:data_map_waxables",
"url": "neoforge/data-map-waxables",
"path": "neoforge/data_map_waxables",
"tags": ["partners"],
"schema": "neoforge:data_map_waxables",
"dependency": "neoforge",
"minVersion": "1.21",
"wiki": "https://docs.neoforged.net/docs/resources/server/datamaps/builtin#neoforgewaxables"
},
{
"id": "neoforge.structure_modifier",
"id": "neoforge:structure_modifier",
"url": "neoforge/structure-modifier",
"path": "neoforge/structure_modifier",
"tags": ["partners"],
"schema": "neoforge:structure_modifier",
"dependency": "neoforge",
"minVersion": "1.20.2",
"wiki": "https://github.com/neoforged/NeoForge/blob/1.21.x/src/main/java/net/neoforged/neoforge/common/world/StructureModifiers.java"
},
{
"id": "obsidian.item",
"url": "obsidian/item",
"path": "obsidian_item",
"tags": ["partners"],
"schema": "obsidian:item"
},
{
"id": "obsidian.block",
"url": "obsidian/block",
"path": "obsidian_block",
"tags": ["partners"],
"schema": "obsidian:block"
},
{
"id": "ohthetreesyoullgrow.configured_feature",
"id": "ohthetreesyoullgrow:configured_feature",
"url": "ohthetreesyoullgrow/feature",
"path": "configured_feature",
"path": "ohthetreesyoullgrow/configured_feature",
"tags": ["partners"],
"schema": "ohthetreesyoullgrow:configured_feature",
"dependency": "ohthetreesyoullgrow",
"minVersion": "1.20",
"wiki": "https://github.com/CorgiTaco/Oh-The-Trees-Youll-Grow/wiki/Generating-Your-Tree-With-Data-Packs!"
}

View File

@@ -3,6 +3,7 @@
"3d": "3D",
"add": "Add",
"add_bottom": "Add to bottom",
"add_key": "Add key",
"add_top": "Add to top",
"any_version": "Any",
"assets": "Assets",
@@ -67,30 +68,29 @@
"generator.error_max_version": "This generator is not available in versions above %0%",
"generator.error_min_version": "The minimum version for this generator is %0%",
"generator.font": "Font",
"generator.immersive_weathering.block_growth": "Block Growth",
"generator.immersive_weathering:block_growth": "Block Growth",
"generator.instrument": "Instrument",
"generator.item_definition": "Item",
"generator.item_modifier": "Item Modifier",
"generator.jukebox_song": "Jukebox Song",
"generator.lithostitched.worldgen_modifier": "Worldgen Modifier",
"generator.lang": "Language",
"generator.loot_table": "Loot Table",
"generator.model": "Model",
"generator.neoforge.biome_modifier": "Biome Modifier",
"generator.neoforge.data_map_compostables": "Compostables Data Map",
"generator.neoforge.data_map_furnace_fuels": "Furnace Fuels Data Map",
"generator.neoforge.data_map_monster_room_mobs": "Monster Room Mobs Data Map",
"generator.neoforge.data_map_oxidizables": "Oxidizables Data Map",
"generator.neoforge.data_map_parrot_imitations": "Parrot Imitations Data Map",
"generator.neoforge.data_map_raid_hero_gifts": "Raid Hero Gifts Data Map",
"generator.neoforge.data_map_vibration_frequencies": "Vibration Frequencies Data Map",
"generator.neoforge.data_map_waxables": "Waxables Data Map",
"generator.neoforge.structure_modifier": "Structure Modifier",
"generator.neoforge:biome_modifier": "Biome Modifier",
"generator.neoforge:data_map_compostables": "Compostables Data Map",
"generator.neoforge:data_map_furnace_fuels": "Furnace Fuels Data Map",
"generator.neoforge:data_map_monster_room_mobs": "Monster Room Mobs Data Map",
"generator.neoforge:data_map_oxidizables": "Oxidizables Data Map",
"generator.neoforge:data_map_parrot_imitations": "Parrot Imitations Data Map",
"generator.neoforge:data_map_raid_hero_gifts": "Raid Hero Gifts Data Map",
"generator.neoforge:data_map_vibration_frequencies": "Vibration Frequencies Data Map",
"generator.neoforge:data_map_waxables": "Waxables Data Map",
"generator.neoforge:structure_modifier": "Structure Modifier",
"generator.not_found": "Cannot find generator \"%0%\"",
"generator.obsidian.block": "Obsidian Block",
"generator.obsidian.item": "Obsidian Item",
"generator.ohthetreesyoullgrow.configured_feature": "OTTYG Feature",
"generator.ohthetreesyoullgrow:configured_feature": "OTTYG Feature",
"generator.pack_mcmeta": "Pack.mcmeta",
"generator.painting_variant": "Painting Variant",
"generator.post_effect": "Post Effect",
"generator.predicate": "Predicate",
"generator.recipe": "Recipe",
"generator.switch_version": "Switch to %0%",
@@ -161,6 +161,7 @@
"layer.vegetation": "Humidity",
"learn_on_the_wiki": "Learn on the wiki",
"loading": "Loading...",
"missing_key": "Missing required key %0%",
"mode.3d": "3D",
"mode.side": "Side",
"mode.top": "Top",
@@ -226,6 +227,7 @@
"reset_default": "Reset to default",
"resource_location": "Resource location",
"restore_backup": "Restore last backup",
"root": "Root",
"search": "Search",
"settings": "Settings",
"settings.fields.description": "Customize advanced field settings",
@@ -288,6 +290,7 @@
"transformation.scale": "Scale",
"transformation.translation": "Translation",
"undo": "Undo",
"unset": "-- unset --",
"version_diff.word_wrap": "Word wrap",
"versions.all": "All versions",
"versions.article": "Article",

View File

@@ -1,7 +1,9 @@
:root {
--background-1: #fafafa;
--background-2: #e2e2e2;
--background-2-shimmer: #d9d9d9;
--background-3: #d4d3d3;
--background-3-shimmer: #cbcbcb;
--background-4: #b8b8b8;
--background-5: #bdbdbd;
--background-6: #cecece;
@@ -53,8 +55,10 @@
:root.dark {
--background-1: #1b1b1b;
--background-2: #252525;
--background-2-shimmer: #2c2c2c;
--background-3: #222222;
--background-4: #3d3d3d;
--background-4-shimmer: #424242;
--background-5: #383838;
--background-6: #575757;
--text-1: #ffffff;
@@ -330,6 +334,7 @@ main > .controls {
.project-controls {
margin: 8px;
margin-right: 0;
display: flex;
z-index: 2;
}
@@ -360,16 +365,48 @@ main > .controls {
z-index: 10;
}
.tree {
.file-view {
margin-top: -40px;
overflow-x: auto;
padding: 8px 16px 50vh;
}
.error + .tree {
.error + .file-view {
margin-top: 0;
}
.skeleton {
opacity: 1;
background-image: linear-gradient(90deg, var(--background-2) 0px, var(--background-2-shimmer) 38px, var(--background-2-shimmer) 42px, var(--background-2) 80px);
background-size: 600px;
animation: skeleton-shimmer 2.5s infinite linear, skeleton-fade-in 1s forwards linear;
}
.skeleton-2 {
opacity: 1;
background-image: linear-gradient(90deg, var(--background-4) 0px, var(--background-4-shimmer) 38px, var(--background-4-shimmer) 42px, var(--background-4) 80px);
background-size: 600px;
animation: skeleton-shimmer 2.5s infinite linear, skeleton-fade-in 1s forwards linear;
}
@keyframes skeleton-shimmer {
0% {
background-position: -110px;
}
40%, 100% {
background-position: 300px;
}
}
@keyframes skeleton-fade-in {
0%, 20% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.popup-source {
position: fixed;
display: flex;
@@ -520,10 +557,8 @@ canvas.preview-details.visible {
.popup-project {
position: fixed;
display: flex;
flex-direction: column;
flex-direction: row;
height: calc(100% - 56px);
width: 200px;
width: max(200px, 20vw);
right: 100%;
bottom: 0;
z-index: 3;
@@ -538,8 +573,20 @@ canvas.preview-details.visible {
}
main.has-project {
padding-left: 200px;
padding-left: max(200px, 20vw);
padding-left: var(--project-panel-width, 200px);
}
.popup-project .panel-content {
display: flex;
flex-direction: column;
width: calc(100% - 8px);
}
.panel-resize {
width: 8px;
height: 100%;
cursor: ew-resize;
user-select: none;
}
.preview-overlay {
@@ -1423,7 +1470,7 @@ main.has-project {
font-size: 16px;
}
[data-modals] .tree {
[data-modals] .file-view {
pointer-events: none;
}
@@ -1775,7 +1822,7 @@ hr {
content: '\200b';
}
.file-view {
.project-files {
background-color: var(--background-2);
color: var(--text-2);
overflow: hidden;
@@ -1784,7 +1831,7 @@ hr {
flex-grow: 1;
}
.file-view > span {
.project-files > span {
padding: 4px 8px;
}
@@ -1932,8 +1979,10 @@ hr {
}
.ea-content {
opacity: 1;
margin: 0 !important;
background: var(--background-2) !important;
animation: skeleton-fade-in 0.5s forwards linear;
}
.ea-content span {
@@ -2967,13 +3016,13 @@ hr {
}
@media screen and (max-width: 1300px) {
main.has-preview .tree {
main.has-preview .file-view {
margin-top: 4px;
}
}
@media screen and (max-width: 800px) {
main .tree {
main .file-view {
margin-top: 4px !important;
}
}
@@ -3004,7 +3053,7 @@ hr {
top: 64px
}
.tree {
.file-view {
padding-left: 8px;
padding-right: 8px;
}

View File

@@ -5,15 +5,16 @@
--node-background-hover: #e7e7e7;
--node-text: #000000;
--node-text-dimmed: #2c2c2c;
--node-selected: #f0e65e;
--node-selected-hover: #faf06c;
--node-selected-border: #b9a327;
--node-selected: #f0bc5c;
--node-selected-hover: #fdce75;
--node-selected-border: #d6a343;
--node-add: #9bd464;
--node-add-hover: #a5dd70;
--node-add-hover: #b0e77c;
--node-add-border: #498d09;
--node-remove: #e76f51;
--node-remove-hover: #f57656;
--node-remove-hover: #f77c5d;
--node-remove-border: #be4b2e;
--node-help: #babcc0;
--node-indent-border: #b9b9b9;
--category-predicate: #65b5b8;
--category-predicate-border: #187e81;
@@ -33,22 +34,23 @@
--node-background-hover: #1f1f1f;
--node-text: #dadada;
--node-text-dimmed: #b4b4b4;
--node-selected: #ad9715;
--node-selected-hover: #a38c0a;
--node-selected-border: #8d7a0d;
--node-selected: #7f5505;
--node-selected-hover: #724c04;
--node-selected-border: #6c4702;
--node-add: #487c13;
--node-add-hover: #3e7409;
--node-add-hover: #396a08;
--node-add-border: #3b6e0c;
--node-remove: #9b341b;
--node-remove-hover: #922d13;
--node-remove-hover: #86270f;
--node-remove-border: #7e1d05;
--node-help: #494949;
--node-indent-border: #454749;
--category-predicate: #306163;
--category-predicate-border: #224849;
--category-predicate-background: #1d3333;
--category-function: #838383;
--category-function-border: #6b6b6b;
--category-function-background: #414141;
--category-function: #5f5f5f;
--category-function-border: #4a4a4a;
--category-function-background: #2c2c2c;
--category-pool: #386330;
--category-pool-border: #2e4922;
--category-pool-background: #21331d;
@@ -72,11 +74,15 @@
.node-header > label {
align-self: flex-start;
padding: 0 9px;
line-height: 1.94rem;
background-color: var(--node-background-label);
}
.node-header > label > span {
padding: 0 9px;
white-space: nowrap;
user-select: none;
background-color: var(--node-background-label);
text-decoration-color: var(--node-text-dimmed);
}
.node-header > label > .item-display {
@@ -84,6 +90,35 @@
height: 32px;
}
.node-doc {
position: absolute;
font-size: 16px;
line-height: 1.3;
z-index: 10;
margin-top: -1px;
margin-left: 3px;
padding: 4px 9px;
border-radius: 4px;
border: 1px solid;
color: var(--node-text-dimmed);
border-color: var(--node-border);
background-color: var(--node-background-label);
box-shadow: 0 1px 7px -2px #000;
}
.node-header > label > span:hover + .node-doc {
display: block;
}
.node-doc code {
color: var(--accent-primary);
}
.node-doc ul {
padding-left: 16px;
list-style-type: disc;
}
.node-header > input {
font-size: 18px;
padding-left: 9px;
@@ -120,11 +155,11 @@
background-color: var(--node-background-input);
}
.node-header button:not([disabled]):hover {
.node-header button:enabled:hover {
background-color: var(--node-background-hover);
}
.node-header a {
.node-header > a {
display: flex;
align-items: center;
font-size: 18px;
@@ -136,14 +171,13 @@
background-color: var(--node-background-input);
}
.object-node > .node-header > .node-collapse {
cursor: pointer;
.node-warning ~ select:last-child,
.node-warning ~ input:last-child {
border-color: var(--node-selected) !important;
}
.node-error ~ select:last-child,
.node-error ~ input:last-child,
.node-error ~ input[list]:nth-last-child(2),
.node-error + .fixed-list ~ input {
.node-error ~ input:last-child {
border-color: var(--node-remove) !important;
}
@@ -159,8 +193,7 @@
border-bottom-left-radius: 3px;
}
.node-header > *:last-child,
.node-header > input[list]:nth-last-child(2) {
.node-header > *:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
@@ -169,54 +202,43 @@
margin-right: -1px;
}
.object-node:not(.no-body) > .node-header > *:first-child,
.map-node > .node-header > *:first-child,
.list-node > .node-header > *:first-child {
border-top-left-radius: 8px;
border-bottom-left-radius: 0;
}
/* Buttons */
button.selected {
background-color: var(--node-selected);
border-color: var(--node-selected-border);
}
button:not([disabled]).selected:hover {
button.selected:enabled:hover {
background-color: var(--node-selected-hover);
}
.node-collapse svg {
fill: var(--node-text);
}
.node-collapse.closed,
button.add {
background-color: var(--node-add);
border-color: var(--node-add-border);
}
.node-collapse:not([disabled]).closed:hover,
button:not([disabled]).add:hover {
button.add:enabled:hover {
background-color: var(--node-add-hover);
}
.node-collapse.open,
button.remove {
background-color: var(--node-remove);
border-color: var(--node-remove-border);
}
.node-collapse:not([disabled]).open:hover,
button:not([disabled]).remove:hover {
button.remove:enabled:hover {
background-color: var(--node-remove-hover);
}
.node-header button:disabled {
cursor: unset;
}
.node-header > button svg {
fill: var(--node-text);
}
.node-header > button.node-collapse:last-child,
.node-header > button.add:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
@@ -289,18 +311,23 @@ button.move:disabled {
}
.node-icon.node-help svg {
fill: var(--node-border);
fill: var(--node-help);
}
.node-icon.node-error svg {
fill: var(--node-remove);
}
.node-icon.node-warning svg {
fill: var(--node-selected);
}
.node-menu {
position: absolute;
left: 0;
top: 100%;
width: min-content;
height: unset;
margin-top: 4px;
margin-left: 4px;
z-index: 1;
@@ -344,50 +371,22 @@ button.move:disabled {
/* Node body and list entry */
.node {
margin-bottom: 4px;
}
.node-body > .node:first-child {
margin-top: 4px;
}
.node:last-child {
margin-bottom: 0;
}
.node-body {
border-left: 3px solid var(--node-indent-border);
.node, .node-root, .node-body-flat {
display: flex;
flex-direction: column;
gap: 4px;
}
.node-body {
display: flex;
flex-direction: column;
gap: 4px;
padding-left: 18px;
border-left: 3px solid var(--node-indent-border);
}
.list-node > .node-body > .object-node > .node-body,
.map-node > .node-body > .object-node > .node-body {
padding-left: 0;
}
.list-node > .node-body > .object-node > .node-body > .node > .node-body,
.map-node > .node-body > .object-node > .node-body > .node > .node-body {
border-left: none;
}
.list-node > .node-body > .object-node > .node-body > .node > .node-header > .node-icon:first-child + *,
.list-node > .node-body > .object-node > .node-body > .node > .node-header > *:first-child,
.map-node > .node-body > .object-node > .node-body > .node > .node-header > .node-icon:first-child + *,
.map-node > .node-body > .object-node > .node-body > .node > .node-header > *:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
.node-body > .object-node[data-category],
.node-body > .list-node[data-category],
.node-body > .map-node[data-category] {
.node-body > .node[data-category],
.node-body-flat > .node[data-category] {
width: 100%;
min-width: max-content;
padding: 5px;
@@ -396,96 +395,98 @@ button.move:disabled {
border-radius: 3px;
}
.node-body > .object-node[data-category] > .node-header > .node-icon:first-child + *,
.node-body > .object-node[data-category] > .node-header > *:first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
.node-body > .node[data-category] > .node-body {
border: none;
}
.node > .node-body-flat > .node > .node-body {
border-left: none;
}
.node-body > .object-node[data-category] > .node-body,
.node-body > .list-node[data-category] > .node-body,
.node-body > .map-node[data-category] > .node-body {
border: none;
.node > .node-body-flat > .node > .node-header > .node-icon + *,
.node > .node-body-flat > .node > .node-header > *:first-child,
.node[data-category] > .node-header > .node-icon + *,
.node[data-category] > .node-header > *:first-child {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.node:not([data-category]) > .node-body-flat {
border-left: 3px solid var(--node-indent-border);
}
/* Node type specifics */
.range-node select {
width: 25px;
}
.fixed-list {
display: none;
}
.number-node input,
.range-node input,
.fixed-list ~ input {
.short-input {
width: 100px;
}
.long-input {
width: 300px;
}
/* Color categories */
[data-category=predicate] > .node-header > label,
[data-category=predicate].node-header > label,
[data-category=predicate] > .node-body > .node > .node-header > label {
[data-category=predicate] > .node-header > button.toggle,
[data-category=predicate] > .node-body > .node > .node-header > label,
[data-category=predicate] > .node-body-flat > .node > .node-header > label {
background-color: var(--category-predicate);
}
[data-category=predicate] > .node-body,
[data-category=predicate] > .node-header > label,
[data-category=predicate].node-header > label,
[data-category=predicate] > .node-header > *:not(.selected),
[data-category=predicate] > .node-body > .node > .node-header > *:not(.selected) {
[data-category=predicate] > .node-body > .node > .node-header > *:not(.selected),
[data-category=predicate] > .node-body-flat > .node > .node-header > *:not(.selected) {
border-color: var(--category-predicate-border);
}
.node-body > .node.object-node[data-category=predicate],
.node-body > .node.list-node[data-category=predicate],
.node-body > .node.map-node[data-category=predicate] {
.node-body > .node[data-category=predicate],
.node-body-flat > .node[data-category=predicate] {
background-color: var(--category-predicate-background);
border-color: var(--category-predicate-border);
}
[data-category=function] > .node-header > label,
[data-category=function].node-header > label,
[data-category=function] > .node-body > .node > .node-header > label {
[data-category=function] > .node-header > button.toggle,
[data-category=function] > .node-body > .node > .node-header > label,
[data-category=function] > .node-body-flat > .node > .node-header > label {
background-color: var(--category-function);
}
[data-category=function] > .node-body,
[data-category=function] > .node-header > label,
[data-category=function].node-header > label,
[data-category=function] > .node-header > *:not(.selected),
[data-category=function] > .node-body > .node > .node-header > *:not(.selected) {
[data-category=function] > .node-body > .node > .node-header > *:not(.selected),
[data-category=function] > .node-body-flat > .node > .node-header > *:not(.selected) {
border-color: var(--category-function-border);
}
.node-body > .node.object-node[data-category=function],
.node-body > .node.list-node[data-category=function],
.node-body > .node.map-node[data-category=function] {
.node-body > .node[data-category=function],
.node-body-flat > .node[data-category=function] {
background-color: var(--category-function-background);
border-color: var(--category-function-border);
}
[data-category=pool] > .node-header > label,
[data-category=pool].node-header > label,
[data-category=pool] > .node-body > .node > .node-header > label {
[data-category=pool] > .node-header > button.toggle,
[data-category=pool] > .node-body > .node > .node-header > label,
[data-category=pool] > .node-body-flat > .node > .node-header > label {
background-color: var(--category-pool);
}
[data-category=pool] > .node-body,
[data-category=pool] > .node-header > label,
[data-category=pool].node-header > label,
[data-category=pool] > .node-header > *:not(.selected),
[data-category=pool] > .node-body > .node > .node-header > *:not(.selected) {
[data-category=pool] > .node-body > .node > .node-header > *:not(.selected),
[data-category=pool] > .node-body-flat > .node > .node-header > *:not(.selected) {
border-color: var(--category-pool-border);
}
.node-body > .node.object-node[data-category=pool],
.node-body > .node.list-node[data-category=pool],
.node-body > .node.map-node[data-category=pool] {
.node-body > .node[data-category=pool],
.node-body-flat > .node[data-category=pool] {
background-color: var(--category-pool-background);
border-color: var(--category-pool-border);
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es6",
"target": "es2021",
"module": "node16",
"lib": ["dom","esnext"],
"moduleResolution": "node16",

View File

@@ -19,8 +19,14 @@ export default defineConfig({
{ find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' },
],
},
optimizeDeps: {
esbuildOptions: {
target: 'es2021',
},
},
build: {
sourcemap: true,
target: 'es2021',
rollupOptions: {
plugins: [
html({