mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
Merge pull request #614 from misode/mcdoc
This commit is contained in:
2226
package-lock.json
generated
2226
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -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",
|
||||
|
||||
44
patches/@spyglassmc+mcdoc+0.3.18.patch
Normal file
44
patches/@spyglassmc+mcdoc+0.3.18.patch
Normal 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) {
|
||||
116
public/mcdoc/immersive_weathering.mcdoc
Normal file
116
public/mcdoc/immersive_weathering.mcdoc
Normal 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
179
public/mcdoc/neoforge.mcdoc
Normal 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,
|
||||
} |
|
||||
)>
|
||||
34
public/mcdoc/ohthetreesyoullgrow.mcdoc
Normal file
34
public/mcdoc/ohthetreesyoullgrow.mcdoc
Normal 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],
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/')
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VersionId } from '../../services/Schemas.js'
|
||||
import type { VersionId } from '../../services/Versions.js'
|
||||
|
||||
export interface CustomizedOreModel {
|
||||
size: number,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
37
src/app/components/generator/FileView.tsx
Normal file
37
src/app/components/generator/FileView.tsx
Normal 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}`} />
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
79
src/app/components/generator/JsonFileView.tsx
Normal file
79
src/app/components/generator/JsonFileView.tsx
Normal 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
|
||||
}
|
||||
}
|
||||
485
src/app/components/generator/McdocHelpers.ts
Normal file
485
src/app/components/generator/McdocHelpers.ts
Normal 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
|
||||
}
|
||||
1232
src/app/components/generator/McdocRenderer.tsx
Normal file
1232
src/app/components/generator/McdocRenderer.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 <></>
|
||||
|
||||
@@ -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)} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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)} />}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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)}/>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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: " />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
40
src/app/contexts/Modal.tsx
Normal file
40
src/app/contexts/Modal.tsx
Normal 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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
76
src/app/contexts/Spyglass.tsx
Normal file
76
src/app/contexts/Spyglass.tsx
Normal 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>
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 ?? []])
|
||||
}
|
||||
@@ -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']
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
}
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
))
|
||||
}
|
||||
@@ -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',
|
||||
])
|
||||
}
|
||||
@@ -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: [],
|
||||
},
|
||||
}),
|
||||
}))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
@@ -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)}`)
|
||||
}
|
||||
|
||||
279
src/app/services/FileSystem.ts
Normal file
279
src/app/services/FileSystem.ts
Normal 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> {}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})()
|
||||
|
||||
@@ -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()
|
||||
})()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
478
src/app/services/Spyglass.ts
Normal file
478
src/app/services/Spyglass.ts
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
13
src/app/services/Versions.ts
Normal file
13
src/app/services/Versions.ts
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
147
src/config.json
147
src/config.json
@@ -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!"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"target": "es2021",
|
||||
"module": "node16",
|
||||
"lib": ["dom","esnext"],
|
||||
"moduleResolution": "node16",
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user